FdcDecimal
FdcDecimal is FDC's exact fixed-scale decimal runtime type. Decimal dataset fields use it to avoid binary floating-point representation errors in money, rates, quantities, percentages, measurements, and other business values where decimal semantics matter.
A decimal value is stored as:
scaledValue × 10^-scale
For example, 123.45 at scale 2 is stored conceptually as:
scaledValue = 12345
scale = 2
value = 12345 × 10^-2 = 123.45
Internally, scaledValue is a BigInt. Supported scale is 0..38.
Why not double?
Binary floating-point values cannot represent many ordinary decimal fractions exactly. For example, values such as 0.1 and 0.2 are approximations in binary floating point.
FdcDecimal keeps decimal values in decimal form throughout parsing, dataset storage, editing, arithmetic, filtering, aggregation, and exact text serialization. Convert to double only at interoperability boundaries that explicitly require floating-point input.
Creating decimal values
The most explicit form is string parsing:
final unitPrice = FdcDecimal.parse('19.95');
final taxRate = FdcDecimal.parse('0.0825');
Convenience extensions are also available:
final quantity = 12.decimal;
final amount = '1234.50'.decimal;
final fixedScale = 100.decimalScale(2); // 100.00
For values where the original decimal text is authoritative, prefer string parsing:
final exact = '0.10000000000000000001'.decimal;
When normalizing to a known field scale:
final amount = FdcDecimal.parse(
'123.456',
scale: 2,
); // 123.46
The same fixed-scale behavior is available through:
final amount = FdcDecimal.parseNormalized(
'123.456',
scale: 2,
);
parseNormalized expects . as the decimal separator. Use parse or tryParse for localized user input.
Precision and scale
FdcDecimalField requires both precision and scale because decimal capacity is part of the dataset schema.
const FdcDecimalField(
name: 'amount',
precision: 12,
scale: 2,
)
For decimal(12, 2):
- precision: up to 12 total decimal digits,
- scale: exactly 2 fractional digits in normalized field storage,
- integer capacity:
12 - 2 = 10digits.
So these values fit:
0.01
123.45
9999999999.99
This value does not fit:
10000000000.00
The field schema validates:
precision: 1..38
scale: 0..precision
Values are rounded to the field scale before precision capacity is checked. For example, assigning 1.999 to a scale-2 field normalizes to 2.00.
Reading and writing decimal fields
Dataset decimal runtime values are FdcDecimal values.
final amount = orders.field('amount').asDecimal;
For writes, assign through the regular field value API:
orders.field('amount').value = '149.95'.decimal;
Field metadata remains available from the typed definition:
final amountDef = orders.fieldDef<FdcDecimalField>('amount');
print(amountDef.precision);
print(amountDef.scale);
Arithmetic operators
FdcDecimal supports decimal-aware arithmetic.
final net = '125.50'.decimal;
final tax = '25.10'.decimal;
final gross = net + tax; // 150.60
final delta = net - tax; // 100.40
final doubled = net * 2; // 251.00
final average = gross / 2; // FdcDecimal
final remainder = gross % 7;
final whole = gross ~/ 7; // int
Operators accept FdcDecimal and finite Dart numeric operands. Integer operands are exact. Finite non-integer num operands are converted through their decimal text representation.
Addition and subtraction
+ and - align both operands to the larger scale and preserve that scale.
1.20 + 3.456 = 4.656
scale 2 + scale 3 → result scale 3
Multiplication
Multiplication uses the sum of operand scales:
1.25 × 2.50 = 3.1250
scale 2 + scale 2 → result scale 4
When the exact multiplication scale would exceed 38, FDC rounds the result back to scale 38.
Division
Division returns an FdcDecimal for finite results. Its result scale is:
max(left.scale, right.scale, 12)
This gives division a minimum of 12 fractional digits while preserving larger operand scales.
final result = 1.decimal / 3.decimal;
print(result); // 0.333333333333
Division rounding is nearest with half values rounded away from zero.
Division by zero is deliberately different from % and ~/:
positive / 0 → Infinity
negative / 0 → -Infinity
0 / 0 → NaN
The / operator returns those non-finite numeric values so calculated-field validation can surface the error instead of aborting calculation immediately.
By contrast, remainder and integer division by zero throw UnsupportedError.
Comparisons and equality
Ordering operators accept decimal-safe numeric operands:
final amount = '10.00'.decimal;
final low = amount < 20;
final sameOrder = amount >= 10.0;
Equality is intentionally stricter:
'10.00'.decimal == '10'.decimal // true
'10.00'.decimal == 10 // false
== only compares FdcDecimal with FdcDecimal. This preserves symmetric Dart equality and correct Set and Map behavior.
For numeric checks, use comparison operators or convert the literal:
final same = amount == 10.decimal;
Decimals with different scales but the same numeric value are equal and have the same hash code:
'10'.decimal == '10.00'.decimal // true
Rounding and integer conversion
FdcDecimal provides explicit integer operations:
final value = '-12.6'.decimal;
value.round(); // -13
value.floor(); // -13
value.ceil(); // -12
value.truncate(); // -12
round() rounds nearest with halves away from zero.
Use toStringAsFixed() when you need exact decimal text at a specific scale:
final value = '12.345'.decimal;
value.toStringAsFixed(); // 12.345
value.toStringAsFixed(2); // 12.35
value.toStringAsFixed(0); // 12
Use toDouble() or toNum() only when interoperating with APIs that require Dart numeric types:
final chartValue = amount.toDouble();
That conversion is an explicit boundary where floating-point approximation can reappear.
Localized parsing
FdcDecimal.parse and tryParse can parse localized separators:
final value = FdcDecimal.parse(
'1.234,56',
decimalSeparator: ',',
thousandSeparator: '.',
scale: 2,
);
For application UI, editors and columns normally obtain separators from resolved FdcFormatSettings. You usually do not need to parse editor text manually.
Decimal fields, editors, and columns
Use schema precision and scale for data capacity:
const FdcDecimalField(
name: 'unit_price',
precision: 10,
scale: 2,
minValue: 0,
)
Use editor and column options for interaction and presentation:
FdcDecimalEdit(
dataSet: products,
fieldName: 'unit_price',
allowNegative: false,
)
const FdcDecimalColumn<dynamic>(
fieldName: 'unit_price',
label: 'Unit price',
prefixText: r'$ ',
)
The division of responsibility is intentional:
| Concern | Configure on |
|---|---|
| precision and scale | FdcDecimalField |
| reusable min/max constraints | FdcDecimalField |
| decimal runtime value | FdcDecimal |
| input sign policy | decimal editor/column |
| decimal and thousands separators | FdcFormatSettings |
| display prefix/suffix | decimal column |
Recommended practices
Use FdcDecimal end-to-end for financial and exact business calculations. Define precision and scale in the field schema, not in presentation widgets. Prefer exact strings when importing externally defined decimal literals. Keep conversions to double at visualization or interoperability boundaries only.