mirror of
https://github.com/we-promise/sure.git
synced 2026-06-05 18:59:04 +00:00
fix(mobile): parse locale-aware transaction amounts (#2130)
* fix(mobile): parse locale-aware transaction amounts * fix(mobile): tighten localized amount parsing
This commit is contained in:
@@ -7,6 +7,7 @@ import '../providers/accounts_provider.dart';
|
||||
import '../providers/transactions_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../services/log_service.dart';
|
||||
import '../utils/amount_parser.dart';
|
||||
|
||||
class CalendarScreen extends StatefulWidget {
|
||||
const CalendarScreen({super.key});
|
||||
@@ -88,10 +89,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
final transactions = transactionsProvider.transactions;
|
||||
_log.info('CalendarScreen', 'Loaded ${transactions.length} transactions for account ${_selectedAccount!.name}');
|
||||
|
||||
if (transactions.isNotEmpty) {
|
||||
_log.debug('CalendarScreen', 'Sample transaction - name: ${transactions.first.name}, amount: ${transactions.first.amount}, nature: ${transactions.first.nature}');
|
||||
}
|
||||
|
||||
// Store transactions for date filtering
|
||||
_transactions = List.from(transactions);
|
||||
|
||||
@@ -114,23 +111,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
final date = DateTime.parse(transaction.date);
|
||||
final dateKey = DateFormat('yyyy-MM-dd').format(date);
|
||||
|
||||
// Parse amount with proper sign handling
|
||||
String trimmedAmount = transaction.amount.trim();
|
||||
trimmedAmount = trimmedAmount.replaceAll('\u2212', '-'); // Normalize minus sign
|
||||
|
||||
// Detect if the amount has a negative sign
|
||||
bool hasNegativeSign = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-');
|
||||
|
||||
// Remove all non-numeric characters except decimal point and minus sign
|
||||
String numericString = trimmedAmount.replaceAll(RegExp(r'[^\d.\-]'), '');
|
||||
|
||||
// Parse the numeric value
|
||||
double amount = double.tryParse(numericString.replaceAll('-', '')) ?? 0.0;
|
||||
|
||||
// Apply the sign from the string
|
||||
if (hasNegativeSign) {
|
||||
amount = -amount;
|
||||
}
|
||||
var amount = AmountParser.parse(transaction.amount).value;
|
||||
|
||||
// For asset accounts, flip the sign to match accounting conventions
|
||||
// For liability accounts, also flip the sign
|
||||
@@ -138,12 +119,12 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
amount = -amount;
|
||||
}
|
||||
|
||||
_log.debug('CalendarScreen', 'Processing transaction ${transaction.name} - date: $dateKey, raw amount: ${transaction.amount}, parsed: $amount, isAsset: ${_selectedAccount?.isAsset}, isLiability: ${_selectedAccount?.isLiability}');
|
||||
_log.debug('CalendarScreen', 'Processing transaction date: $dateKey, parsed amount sign adjusted');
|
||||
|
||||
changes[dateKey] = (changes[dateKey] ?? 0.0) + amount;
|
||||
_log.debug('CalendarScreen', 'Date $dateKey now has total: ${changes[dateKey]}');
|
||||
} catch (e) {
|
||||
_log.error('CalendarScreen', 'Failed to parse transaction date: ${transaction.date}, error: $e');
|
||||
} catch (_) {
|
||||
_log.error('CalendarScreen', 'Failed to process transaction for calendar');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,9 +229,12 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
|
||||
Widget _buildTransactionTile(Transaction transaction) {
|
||||
// Parse amount to determine if positive or negative
|
||||
String trimmedAmount = transaction.amount.trim();
|
||||
trimmedAmount = trimmedAmount.replaceAll('\u2212', '-');
|
||||
bool isNegative = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-');
|
||||
var isNegative = false;
|
||||
try {
|
||||
isNegative = AmountParser.parse(transaction.amount).value < 0;
|
||||
} on FormatException {
|
||||
// Keep the dialog renderable if the server returns a malformed amount.
|
||||
}
|
||||
|
||||
// For asset accounts, flip the sign interpretation
|
||||
if (_selectedAccount?.isAsset == true || _selectedAccount?.isLiability == true) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../models/account.dart';
|
||||
import '../providers/transactions_provider.dart';
|
||||
import '../providers/accounts_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../utils/amount_parser.dart';
|
||||
|
||||
class RecentTransactionsScreen extends StatefulWidget {
|
||||
const RecentTransactionsScreen({super.key});
|
||||
@@ -176,35 +177,28 @@ class _RecentTransactionsScreenState extends State<RecentTransactionsScreen> {
|
||||
final account = _getAccount(transaction.accountId);
|
||||
final accountName = account?.name ?? 'Unknown Account';
|
||||
|
||||
// Parse amount with proper sign handling (same logic as transactions_list_screen.dart)
|
||||
String trimmedAmount = transaction.amount.trim();
|
||||
trimmedAmount = trimmedAmount.replaceAll('\u2212', '-'); // Normalize minus sign
|
||||
|
||||
// Detect if the amount has a negative sign
|
||||
bool hasNegativeSign = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-');
|
||||
|
||||
// Remove all non-numeric characters except decimal point and minus sign
|
||||
String numericString = trimmedAmount.replaceAll(RegExp(r'[^\d.\-]'), '');
|
||||
|
||||
// Parse the numeric value
|
||||
double amount = double.tryParse(numericString.replaceAll('-', '')) ?? 0.0;
|
||||
|
||||
// Apply the sign from the string
|
||||
if (hasNegativeSign) {
|
||||
amount = -amount;
|
||||
double? amount;
|
||||
try {
|
||||
amount = AmountParser.parse(transaction.amount).value;
|
||||
} on FormatException {
|
||||
// Keep the list renderable if the server returns a malformed amount.
|
||||
}
|
||||
|
||||
// For asset accounts and liability accounts, flip the sign to match accounting conventions
|
||||
if (account?.isAsset == true || account?.isLiability == true) {
|
||||
if (amount != null &&
|
||||
(account?.isAsset == true || account?.isLiability == true)) {
|
||||
amount = -amount;
|
||||
}
|
||||
|
||||
// Determine display properties based on final amount
|
||||
final isPositive = amount >= 0;
|
||||
final isPositive = amount == null || amount >= 0;
|
||||
Color amountColor;
|
||||
String sign;
|
||||
|
||||
if (isPositive) {
|
||||
if (amount == null) {
|
||||
amountColor = Colors.grey;
|
||||
sign = '';
|
||||
} else if (isPositive) {
|
||||
amountColor = Colors.green.shade700;
|
||||
sign = '+';
|
||||
} else {
|
||||
@@ -225,13 +219,19 @@ class _RecentTransactionsScreenState extends State<RecentTransactionsScreen> {
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isPositive
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Colors.red.withValues(alpha: 0.1),
|
||||
color: amount == null
|
||||
? Colors.grey.withValues(alpha: 0.1)
|
||||
: isPositive
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
isPositive ? Icons.arrow_upward : Icons.arrow_downward,
|
||||
amount == null
|
||||
? Icons.help_outline
|
||||
: isPositive
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
color: amountColor,
|
||||
),
|
||||
),
|
||||
@@ -274,7 +274,9 @@ class _RecentTransactionsScreenState extends State<RecentTransactionsScreen> {
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
'$sign${transaction.currency} ${_formatAmount(amount.abs())}',
|
||||
amount == null
|
||||
? transaction.amount
|
||||
: '$sign${transaction.currency} ${_formatAmount(amount.abs())}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../providers/categories_provider.dart';
|
||||
import '../providers/transactions_provider.dart';
|
||||
import '../services/log_service.dart';
|
||||
import '../services/connectivity_service.dart';
|
||||
import '../utils/amount_parser.dart';
|
||||
|
||||
class TransactionFormScreen extends StatefulWidget {
|
||||
final Account account;
|
||||
@@ -66,8 +67,10 @@ class _TransactionFormScreenState extends State<TransactionFormScreen> {
|
||||
return 'Please enter an amount';
|
||||
}
|
||||
|
||||
final amount = double.tryParse(value.trim());
|
||||
if (amount == null) {
|
||||
final double amount;
|
||||
try {
|
||||
amount = AmountParser.parse(value, locale: _currentLocaleName()).value;
|
||||
} on FormatException {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
|
||||
@@ -78,6 +81,11 @@ class _TransactionFormScreenState extends State<TransactionFormScreen> {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _currentLocaleName() {
|
||||
return Localizations.maybeLocaleOf(context)?.toString() ??
|
||||
Intl.getCurrentLocale();
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -126,6 +134,10 @@ class _TransactionFormScreenState extends State<TransactionFormScreen> {
|
||||
// Convert date format from yyyy/MM/dd to yyyy-MM-dd
|
||||
final parsedDate = DateFormat('yyyy/MM/dd').parse(_dateController.text);
|
||||
final apiDate = DateFormat('yyyy-MM-dd').format(parsedDate);
|
||||
final canonicalAmount = AmountParser.canonicalize(
|
||||
_amountController.text,
|
||||
locale: _currentLocaleName(),
|
||||
);
|
||||
|
||||
_log.info('TransactionForm', 'Calling TransactionsProvider.createTransaction (offline-first)');
|
||||
|
||||
@@ -135,7 +147,7 @@ class _TransactionFormScreenState extends State<TransactionFormScreen> {
|
||||
accountId: widget.account.id,
|
||||
name: _nameController.text.trim(),
|
||||
date: apiDate,
|
||||
amount: _amountController.text.trim(),
|
||||
amount: canonicalAmount,
|
||||
currency: widget.account.currency,
|
||||
nature: _nature,
|
||||
notes: 'This transaction via mobile app.',
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../screens/transaction_form_screen.dart';
|
||||
import '../widgets/category_filter.dart';
|
||||
import '../widgets/sync_status_badge.dart';
|
||||
import '../services/log_service.dart';
|
||||
import '../utils/amount_parser.dart';
|
||||
|
||||
class TransactionsListScreen extends StatefulWidget {
|
||||
final Account account;
|
||||
@@ -39,52 +40,32 @@ class _TransactionsListScreenState extends State<TransactionsListScreen> {
|
||||
// Amount is a currency-formatted string returned by the API (e.g. may include
|
||||
// currency symbol, grouping separators, locale-dependent decimal separator,
|
||||
// and a sign either before or after the symbol)
|
||||
Map<String, dynamic> _getAmountDisplayInfo(String amount, bool isAsset) {
|
||||
Map<String, dynamic> _getAmountDisplayInfo(
|
||||
String amount,
|
||||
bool isAsset,
|
||||
bool isLiability,
|
||||
) {
|
||||
try {
|
||||
// Trim whitespace
|
||||
String trimmedAmount = amount.trim();
|
||||
final parsed = AmountParser.parse(amount);
|
||||
var numericValue = parsed.value;
|
||||
|
||||
// Normalize common minus characters (U+002D HYPHEN-MINUS, U+2212 MINUS SIGN)
|
||||
trimmedAmount = trimmedAmount.replaceAll('\u2212', '-');
|
||||
|
||||
// Detect if the amount has a negative sign (leading or trailing)
|
||||
bool hasNegativeSign = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-');
|
||||
|
||||
// Remove all non-numeric characters except decimal point and minus sign
|
||||
String numericString = trimmedAmount.replaceAll(RegExp(r'[^\d.\-]'), '');
|
||||
|
||||
// Parse the numeric value
|
||||
double numericValue = double.tryParse(numericString.replaceAll('-', '')) ?? 0.0;
|
||||
|
||||
// Apply the sign from the string
|
||||
if (hasNegativeSign) {
|
||||
numericValue = -numericValue;
|
||||
}
|
||||
|
||||
// For asset accounts, flip the sign to match accounting conventions
|
||||
if (isAsset) {
|
||||
// For asset and liability accounts, flip the sign to match accounting conventions
|
||||
if (isAsset || isLiability) {
|
||||
numericValue = -numericValue;
|
||||
}
|
||||
|
||||
// Determine if the final value is positive
|
||||
bool isPositive = numericValue >= 0;
|
||||
|
||||
// Get the display amount by removing the sign and currency symbols
|
||||
String displayAmount = trimmedAmount
|
||||
.replaceAll('-', '')
|
||||
.replaceAll('\u2212', '')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
'isPositive': isPositive,
|
||||
'displayAmount': displayAmount,
|
||||
'displayAmount': parsed.displayText,
|
||||
'color': isPositive ? Colors.green : Colors.red,
|
||||
'icon': isPositive ? Icons.arrow_upward : Icons.arrow_downward,
|
||||
'prefix': isPositive ? '' : '-',
|
||||
};
|
||||
} catch (e) {
|
||||
// Fallback if parsing fails - log and return neutral state
|
||||
LogService.instance.error('TransactionsListScreen', 'Failed to parse amount "$amount": $e');
|
||||
} on FormatException {
|
||||
LogService.instance.error('TransactionsListScreen', 'Failed to parse transaction amount');
|
||||
return {
|
||||
'isPositive': true,
|
||||
'displayAmount': amount,
|
||||
@@ -496,6 +477,7 @@ class _TransactionsListScreenState extends State<TransactionsListScreen> {
|
||||
final displayInfo = _getAmountDisplayInfo(
|
||||
transaction.amount,
|
||||
widget.account.isAsset,
|
||||
widget.account.isLiability,
|
||||
);
|
||||
|
||||
return Dismissible(
|
||||
|
||||
204
mobile/lib/utils/amount_parser.dart
Normal file
204
mobile/lib/utils/amount_parser.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ParsedAmount {
|
||||
const ParsedAmount({
|
||||
required this.value,
|
||||
required this.canonicalValue,
|
||||
required this.displayText,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final String canonicalValue;
|
||||
final String displayText;
|
||||
}
|
||||
|
||||
class AmountParser {
|
||||
static ParsedAmount parse(String input, {String? locale}) {
|
||||
final normalized = _normalizeMinus(input.trim());
|
||||
final negative = _isNegative(normalized);
|
||||
final numericText = normalized.replaceAll(RegExp(r'[^0-9.,]'), '');
|
||||
|
||||
if (!RegExp(r'\d').hasMatch(numericText)) {
|
||||
throw const FormatException('Amount must contain digits');
|
||||
}
|
||||
|
||||
final separators = _Separators.forLocale(locale);
|
||||
final decimalSeparator = _decimalSeparatorFor(numericText, separators);
|
||||
final canonical = _canonicalize(
|
||||
numericText,
|
||||
decimalSeparator: decimalSeparator,
|
||||
negative: negative,
|
||||
);
|
||||
|
||||
return ParsedAmount(
|
||||
value: double.parse(canonical),
|
||||
canonicalValue: canonical,
|
||||
displayText: _displayText(normalized),
|
||||
);
|
||||
}
|
||||
|
||||
static String canonicalize(String input, {String? locale}) {
|
||||
return parse(input, locale: locale).canonicalValue;
|
||||
}
|
||||
|
||||
static String _normalizeMinus(String value) {
|
||||
return value
|
||||
.replaceAll('\u2212', '-')
|
||||
.replaceAll('\u2012', '-')
|
||||
.replaceAll('\u2013', '-')
|
||||
.replaceAll('\u2014', '-');
|
||||
}
|
||||
|
||||
static bool _isNegative(String value) {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.contains('-') ||
|
||||
(trimmed.startsWith('(') && trimmed.endsWith(')'));
|
||||
}
|
||||
|
||||
static String _displayText(String value) {
|
||||
var display = value.trim();
|
||||
|
||||
if (display.startsWith('(') && display.endsWith(')')) {
|
||||
display = display.substring(1, display.length - 1).trim();
|
||||
}
|
||||
|
||||
display = display.replaceAll(RegExp(r'[-+]'), '').trim();
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
static String _canonicalize(
|
||||
String numericText, {
|
||||
required String? decimalSeparator,
|
||||
required bool negative,
|
||||
}) {
|
||||
String integerDigits;
|
||||
String fractionDigits = '';
|
||||
|
||||
if (decimalSeparator == null) {
|
||||
integerDigits = numericText.replaceAll(RegExp(r'\D'), '');
|
||||
} else {
|
||||
final decimalIndex = numericText.lastIndexOf(decimalSeparator);
|
||||
integerDigits =
|
||||
numericText.substring(0, decimalIndex).replaceAll(RegExp(r'\D'), '');
|
||||
fractionDigits =
|
||||
numericText.substring(decimalIndex + 1).replaceAll(RegExp(r'\D'), '');
|
||||
}
|
||||
|
||||
integerDigits = integerDigits.replaceFirst(RegExp(r'^0+(?=\d)'), '');
|
||||
if (integerDigits.isEmpty) {
|
||||
integerDigits = '0';
|
||||
}
|
||||
|
||||
fractionDigits = fractionDigits.replaceFirst(RegExp(r'0+$'), '');
|
||||
|
||||
final unsigned = fractionDigits.isEmpty
|
||||
? integerDigits
|
||||
: '$integerDigits.$fractionDigits';
|
||||
|
||||
if (!negative || unsigned == '0') {
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
return '-$unsigned';
|
||||
}
|
||||
|
||||
static String? _decimalSeparatorFor(
|
||||
String numericText,
|
||||
_Separators separators,
|
||||
) {
|
||||
final lastDot = numericText.lastIndexOf('.');
|
||||
final lastComma = numericText.lastIndexOf(',');
|
||||
|
||||
if (lastDot == -1 && lastComma == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastDot != -1 && lastComma != -1) {
|
||||
return lastDot > lastComma ? '.' : ',';
|
||||
}
|
||||
|
||||
final separator = lastDot == -1 ? ',' : '.';
|
||||
final parts = numericText.split(separator);
|
||||
|
||||
if (parts.length > 2) {
|
||||
if (_looksGrouped(parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw const FormatException('Invalid amount format');
|
||||
}
|
||||
|
||||
if (separator == separators.decimalSeparator) {
|
||||
return separator;
|
||||
}
|
||||
|
||||
final lastGroupLength = parts.last.length;
|
||||
if (separator == separators.groupSeparator && lastGroupLength == 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_looksGrouped(parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return separator;
|
||||
}
|
||||
|
||||
static bool _looksGrouped(List<String> parts) {
|
||||
if (parts.length < 2 || parts.first.isEmpty || parts.first.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts.skip(1).every((part) => part.length == 3);
|
||||
}
|
||||
}
|
||||
|
||||
class _Separators {
|
||||
const _Separators({
|
||||
required this.decimalSeparator,
|
||||
required this.groupSeparator,
|
||||
});
|
||||
|
||||
final String decimalSeparator;
|
||||
final String groupSeparator;
|
||||
|
||||
static _Separators forLocale(String? locale) {
|
||||
final effectiveLocale = locale ?? Intl.getCurrentLocale();
|
||||
|
||||
try {
|
||||
final symbols = NumberFormat.decimalPattern(effectiveLocale).symbols;
|
||||
return _Separators(
|
||||
decimalSeparator: symbols.DECIMAL_SEP,
|
||||
groupSeparator: symbols.GROUP_SEP,
|
||||
);
|
||||
} catch (_) {
|
||||
if (_usesDecimalComma(effectiveLocale)) {
|
||||
return const _Separators(decimalSeparator: ',', groupSeparator: '.');
|
||||
}
|
||||
|
||||
return const _Separators(decimalSeparator: '.', groupSeparator: ',');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback heuristic for common decimal-comma locales when NumberFormat's
|
||||
// locale database cannot resolve the requested locale. Keep the set limited
|
||||
// to languages we intentionally support and extend it with tests as needed.
|
||||
static bool _usesDecimalComma(String locale) {
|
||||
final language = locale.split(RegExp(r'[-_]')).first.toLowerCase();
|
||||
return {
|
||||
'ca',
|
||||
'de',
|
||||
'es',
|
||||
'fr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
'ro',
|
||||
'tr',
|
||||
}.contains(language);
|
||||
}
|
||||
}
|
||||
82
mobile/test/utils/amount_parser_test.dart
Normal file
82
mobile/test/utils/amount_parser_test.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sure_mobile/utils/amount_parser.dart';
|
||||
|
||||
void main() {
|
||||
group('AmountParser', () {
|
||||
test('parses decimal-comma currency values', () {
|
||||
final amount = AmountParser.parse('Rp1.234,56', locale: 'id_ID');
|
||||
|
||||
expect(amount.value, 1234.56);
|
||||
expect(amount.canonicalValue, '1234.56');
|
||||
expect(amount.displayText, 'Rp1.234,56');
|
||||
});
|
||||
|
||||
test('parses grouped zero-decimal locale values as whole amounts', () {
|
||||
final amount = AmountParser.parse('1.234.567', locale: 'id_ID');
|
||||
|
||||
expect(amount.value, 1234567);
|
||||
expect(amount.canonicalValue, '1234567');
|
||||
});
|
||||
|
||||
test('parses decimal-dot currency values', () {
|
||||
final amount = AmountParser.parse(r'$1,234.56', locale: 'en_US');
|
||||
|
||||
expect(amount.value, 1234.56);
|
||||
expect(amount.canonicalValue, '1234.56');
|
||||
expect(amount.displayText, r'$1,234.56');
|
||||
});
|
||||
|
||||
test('uses locale separators for ambiguous single separators', () {
|
||||
expect(
|
||||
AmountParser.parse('1.234', locale: 'id_ID').canonicalValue,
|
||||
'1234',
|
||||
);
|
||||
expect(
|
||||
AmountParser.parse('1.234', locale: 'en_US').canonicalValue,
|
||||
'1.234',
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizes minus variants and removes sign from display text', () {
|
||||
final amount = AmountParser.parse('\u2212Rp1.234,50', locale: 'id_ID');
|
||||
|
||||
expect(amount.value, -1234.5);
|
||||
expect(amount.canonicalValue, '-1234.5');
|
||||
expect(amount.displayText, 'Rp1.234,50');
|
||||
});
|
||||
|
||||
test('removes embedded and trailing signs from display text', () {
|
||||
expect(
|
||||
AmountParser.parse('Rp-1.234,50', locale: 'id_ID').displayText,
|
||||
'Rp1.234,50',
|
||||
);
|
||||
expect(
|
||||
AmountParser.parse('Rp1.234,50-', locale: 'id_ID').displayText,
|
||||
'Rp1.234,50',
|
||||
);
|
||||
});
|
||||
|
||||
test('canonicalizes transaction form input for API payloads', () {
|
||||
expect(AmountParser.canonicalize('1.234,50', locale: 'id_ID'), '1234.5');
|
||||
expect(AmountParser.canonicalize('1,234.50', locale: 'en_US'), '1234.5');
|
||||
});
|
||||
|
||||
test('rejects repeated separators that are not grouped', () {
|
||||
expect(
|
||||
() => AmountParser.parse('1.2.3', locale: 'en_US'),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => AmountParser.parse('1,2,3', locale: 'id_ID'),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects inputs without digits', () {
|
||||
expect(
|
||||
() => AmountParser.parse('USD', locale: 'en_US'),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user