feat(core): add number format model and formatting
This commit is contained in:
1 parent
7e998cde53
commit
a027d2362f
1 file changed
+173
@@ -0,0 +1,173 @@
|
|||||||
|
import 'values.dart';
|
||||||
|
|
||||||
|
enum FormatKind { general, number, percent, currency, scientific, integer }
|
||||||
|
|
||||||
|
class NumberFormat {
|
||||||
|
final FormatKind kind;
|
||||||
|
final int decimals;
|
||||||
|
final bool thousands;
|
||||||
|
final String currencySymbol;
|
||||||
|
final bool symbolBefore;
|
||||||
|
|
||||||
|
const NumberFormat({
|
||||||
|
required this.kind,
|
||||||
|
this.decimals = 2,
|
||||||
|
this.thousands = false,
|
||||||
|
this.currencySymbol = '',
|
||||||
|
this.symbolBefore = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const NumberFormat.general() : this(kind: FormatKind.general);
|
||||||
|
|
||||||
|
const NumberFormat.integer({bool thousands = false})
|
||||||
|
: this(kind: FormatKind.integer, decimals: 0, thousands: thousands);
|
||||||
|
|
||||||
|
const NumberFormat.decimal({int decimals = 2, bool thousands = false})
|
||||||
|
: this(kind: FormatKind.number, decimals: decimals, thousands: thousands);
|
||||||
|
|
||||||
|
const NumberFormat.percent({int decimals = 0})
|
||||||
|
: this(kind: FormatKind.percent, decimals: decimals);
|
||||||
|
|
||||||
|
const NumberFormat.currency(
|
||||||
|
String symbol, {
|
||||||
|
int decimals = 2,
|
||||||
|
bool before = true,
|
||||||
|
bool thousands = true,
|
||||||
|
}) : this(
|
||||||
|
kind: FormatKind.currency,
|
||||||
|
decimals: decimals,
|
||||||
|
thousands: thousands,
|
||||||
|
currencySymbol: symbol,
|
||||||
|
symbolBefore: before,
|
||||||
|
);
|
||||||
|
|
||||||
|
String apply(CellValue value) {
|
||||||
|
if (value is! NumberValue) return value.display();
|
||||||
|
final raw = value.value;
|
||||||
|
switch (kind) {
|
||||||
|
case FormatKind.general:
|
||||||
|
return formatNumber(raw);
|
||||||
|
case FormatKind.integer:
|
||||||
|
return _fixed(raw, 0, thousands);
|
||||||
|
case FormatKind.number:
|
||||||
|
return _fixed(raw, decimals, thousands);
|
||||||
|
case FormatKind.percent:
|
||||||
|
return '${_fixed(raw * 100, decimals, false)}%';
|
||||||
|
case FormatKind.currency:
|
||||||
|
final body = _fixed(raw, decimals, thousands);
|
||||||
|
return symbolBefore ? '$currencySymbol$body' : '$body$currencySymbol';
|
||||||
|
case FormatKind.scientific:
|
||||||
|
return raw.toStringAsExponential(decimals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String toCode() {
|
||||||
|
switch (kind) {
|
||||||
|
case FormatKind.general:
|
||||||
|
return 'general';
|
||||||
|
case FormatKind.integer:
|
||||||
|
return thousands ? '#,##0' : '0';
|
||||||
|
case FormatKind.number:
|
||||||
|
return '${thousands ? '#,##0' : '0'}${_decimalSuffix()}';
|
||||||
|
case FormatKind.percent:
|
||||||
|
return '0${_decimalSuffix()}%';
|
||||||
|
case FormatKind.currency:
|
||||||
|
final body = '${thousands ? '#,##0' : '0'}${_decimalSuffix()}';
|
||||||
|
return symbolBefore ? '$currencySymbol$body' : '$body$currencySymbol';
|
||||||
|
case FormatKind.scientific:
|
||||||
|
return '0${_decimalSuffix()}E+0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _decimalSuffix() => decimals == 0 ? '' : '.${'0' * decimals}';
|
||||||
|
|
||||||
|
static NumberFormat fromCode(String code) {
|
||||||
|
if (code == 'general' || code.isEmpty) return const NumberFormat.general();
|
||||||
|
if (code.endsWith('%')) {
|
||||||
|
return NumberFormat.percent(decimals: _countDecimals(code));
|
||||||
|
}
|
||||||
|
if (code.contains('E+') || code.contains('e+')) {
|
||||||
|
return NumberFormat(kind: FormatKind.scientific, decimals: _countDecimals(code));
|
||||||
|
}
|
||||||
|
final leading = _leadingSymbol(code);
|
||||||
|
final trailing = _trailingSymbol(code);
|
||||||
|
final thousands = code.contains(',');
|
||||||
|
final decimals = _countDecimals(code);
|
||||||
|
if (leading.isNotEmpty) {
|
||||||
|
return NumberFormat.currency(leading, decimals: decimals, thousands: thousands);
|
||||||
|
}
|
||||||
|
if (trailing.isNotEmpty) {
|
||||||
|
return NumberFormat.currency(trailing, decimals: decimals, before: false, thousands: thousands);
|
||||||
|
}
|
||||||
|
if (decimals == 0) return NumberFormat.integer(thousands: thousands);
|
||||||
|
return NumberFormat.decimal(decimals: decimals, thousands: thousands);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _countDecimals(String code) {
|
||||||
|
final dot = code.indexOf('.');
|
||||||
|
if (dot < 0) return 0;
|
||||||
|
var count = 0;
|
||||||
|
for (var i = dot + 1; i < code.length; i++) {
|
||||||
|
if (code[i] == '0' || code[i] == '#') {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _leadingSymbol(String code) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final rune in code.runes) {
|
||||||
|
final ch = String.fromCharCode(rune);
|
||||||
|
if ('0#,.'.contains(ch)) break;
|
||||||
|
buffer.write(ch);
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _trailingSymbol(String code) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final rune in code.runes.toList().reversed) {
|
||||||
|
final ch = String.fromCharCode(rune);
|
||||||
|
if ('0#,.%'.contains(ch)) break;
|
||||||
|
buffer.write(ch);
|
||||||
|
}
|
||||||
|
return String.fromCharCodes(buffer.toString().runes.toList().reversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is NumberFormat &&
|
||||||
|
other.kind == kind &&
|
||||||
|
other.decimals == decimals &&
|
||||||
|
other.thousands == thousands &&
|
||||||
|
other.currencySymbol == currencySymbol &&
|
||||||
|
other.symbolBefore == symbolBefore;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(kind, decimals, thousands, currencySymbol, symbolBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fixed(double value, int decimals, bool thousands) {
|
||||||
|
if (value.isNaN) return ErrorKind.numError.symbol;
|
||||||
|
if (value.isInfinite) return value.isNegative ? '-Infinity' : 'Infinity';
|
||||||
|
final negative = value < 0;
|
||||||
|
final text = value.abs().toStringAsFixed(decimals);
|
||||||
|
final dot = text.indexOf('.');
|
||||||
|
final intPart = dot < 0 ? text : text.substring(0, dot);
|
||||||
|
final fracPart = dot < 0 ? '' : text.substring(dot);
|
||||||
|
final grouped = thousands ? _group(intPart) : intPart;
|
||||||
|
return '${negative ? '-' : ''}$grouped$fracPart';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _group(String digits) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
final length = digits.length;
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
if (i > 0 && (length - i) % 3 == 0) buffer.write(',');
|
||||||
|
buffer.write(digits[i]);
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user