quantityFromString method

double? quantityFromString({
  1. required String regionString,
})

Parses numbers written with either EU or US separators. Rules:

  • Rightmost of ., is decimal when both exist.
  • Single separator: 1–2 digits after => decimal.
  • Single separator: exactly '000' after => decimal (so '1.000'/'1,000' => 1.0).
  • Else treat separators as thousands.

Implementation

double? quantityFromString({required String regionString}) {
  String s = trim();
  if (s.isEmpty) return null;

  // Remove spaces (incl. NBSP) and underscores used as thousand separators
  s = s.replaceAll(RegExp(r'[\s\u00A0_]'), '');
  // Keep only digits, sign, and separators
  s = s.replaceAll(RegExp(r'[^0-9,.\-+]'), '');
  if (s.isEmpty) return null;

  int count(String ch) => s.split(ch).length - 1;

  final hasDot = s.contains('.');
  final hasComma = s.contains(',');

  String? decimalSep;

  if (hasDot && hasComma) {
    // Decimal is whichever comes LAST
    decimalSep = s.lastIndexOf('.') > s.lastIndexOf(',') ? '.' : ',';
  } else if (hasDot || hasComma) {
    final sep = hasDot ? '.' : ',';
    final parts = s.split(sep);
    final after = parts.last;
    final sepCount = count(sep);

    bool allZeros(String x) => RegExp(r'^0+$').hasMatch(x);

    if (sepCount > 1) {
      // Like 1.234.567 or 1,234,567 or 1.234.56
      // If the last group length <= 2 => last is decimal, others are thousands
      if (after.length <= 2) {
        decimalSep = sep;
      } else if (after.length == 3 && allZeros(after)) {
        // 1.000 / 1,000 must be 1.0
        decimalSep = sep;
      } else {
        // Likely all thousands groups
        decimalSep = null;
      }
    } else {
      // Only one separator in the whole string
      if (after.length <= 2) {
        decimalSep = sep;
      } else if (after.length == 3 && allZeros(after)) {
        // Explicitly make 1.000 / 1,000 => 1.0
        decimalSep = sep;
      } else {
        decimalSep = null; // thousands
      }
    }
  }

  // Normalize to a plain "[-]digits[.digits]" shape
  String normalized;
  if (decimalSep != null) {
    final lastIdx = s.lastIndexOf(decimalSep);
    var intPart = s.substring(0, lastIdx).replaceAll(RegExp(r'[,.]'), '');
    var fracPart = s.substring(lastIdx + 1).replaceAll(RegExp(r'[^0-9]'), '');
    // If fractional was exactly '000', reduce to a single '0'
    if (fracPart == '000') fracPart = '0';
    if (intPart.isEmpty) intPart = '0';

    // Keep sign if present at the very start
    final sign = s.startsWith('-') ? '-' : (s.startsWith('+') ? '+' : '');
    normalized = '$sign$intPart.$fracPart';
  } else {
    // No decimal — just remove separators as thousands and keep sign
    final sign = s.startsWith('-') ? '-' : (s.startsWith('+') ? '+' : '');
    final digits = s.replaceAll(RegExp(r'\D'), '');
    normalized = '$sign$digits';
  }

  return double.tryParse(normalized);
}