Ctrl K
ComunidadGuías…

shared/utils/currency.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Common currency symbols used around the world.
 */
const currencySymbols = [
  "$", // Dollar (USD, CAD, AUD, etc.)
  "€", // Euro
  "£", // Pound
  "¥", // Yen/Yuan
  "₹", // Indian Rupee
  "₽", // Russian Ruble
  "₿", // Bitcoin
  "₩", // Korean Won
  "₪", // Israeli Shekel
  "₺", // Turkish Lira
  "₴", // Ukrainian Hryvnia
  "₱", // Philippine Peso
  "฿", // Thai Baht
  "₫", // Vietnamese Dong
  "₦", // Nigerian Naira
  "₵", // Ghanaian Cedi
  "₡", // Costa Rican Colón
  "₲", // Paraguayan Guaraní
  "₸", // Kazakhstani Tenge
  "₼", // Azerbaijani Manat
  "₾", // Georgian Lari
  "৳", // Bangladeshi Taka
  "₠", // European Currency Unit
  "R$", // Brazilian Real
  "kr", // Scandinavian Krona/Krone
  "zł", // Polish Zloty
  "Kč", // Czech Koruna
  "Ft", // Hungarian Forint
  "CHF", // Swiss Franc
  "лв", // Bulgarian Lev
  "lei", // Romanian Leu
  "ден", // Macedonian Denar
  "дин", // Serbian Dinar
  "ر.س", // Saudi Riyal
  "د.إ", // UAE Dirham
  "ر.ع", // Omani Rial
  "د.ك", // Kuwaiti Dinar
  "د.ب", // Bahraini Dinar
  "ر.ق", // Qatari Riyal
];

/**
 * Checks if a string appears to be a currency value.
 * Matches formats like: $1,234.56, €50, £1.000,50, -$500, ($500), 1234¥
 *
 * @param value - the string to check.
 * @returns true if the string appears to be a currency value.
 */
export function isCurrency(value: string): boolean {
  if (!value || value.trim().length === 0) {
    return false;
  }

  const trimmed = value.trim();

  // Must contain at least one currency symbol
  const hasCurrencySymbol = currencySymbols.some((symbol) =>
    trimmed.includes(symbol)
  );
  if (!hasCurrencySymbol) {
    return false;
  }

  // Must contain at least one digit
  if (!/\d/.test(trimmed)) {
    return false;
  }

  // Remove all valid currency characters and check if anything unexpected remains
  let remaining = trimmed;

  // Remove currency symbols (longest first to handle multi-char symbols like R$)
  const sortedSymbols = [...currencySymbols].sort(
    (a, b) => b.length - a.length
  );
  for (const symbol of sortedSymbols) {
    remaining = remaining.split(symbol).join("");
  }

  // Remove digits, separators, whitespace, and negative indicators
  remaining = remaining.replace(/[\d.,\s()-]/g, "");

  // If anything remains, it's not a valid currency
  return remaining.length === 0;
}

/**
 * Parses a currency string and returns its numeric value.
 * Handles various formats including:
 * - US/UK style: $1,234.56
 * - European style: €1.234,56
 * - Negative values: -$500, ($500), -500€
 * - Currency symbol before or after the number
 *
 * @param value - the currency string to parse.
 * @returns the numeric value, or null if parsing fails.
 */
export function parseCurrency(value: string): number | null {
  if (!value || value.trim().length === 0) {
    return null;
  }

  // Remove currency symbols and whitespace
  // Sort symbols by length descending so multi-character symbols (like R$) are removed first
  let cleaned = value.trim();
  const sortedSymbols = [...currencySymbols].sort(
    (a, b) => b.length - a.length
  );
  for (const symbol of sortedSymbols) {
    cleaned = cleaned.split(symbol).join("");
  }
  cleaned = cleaned.replace(/\s/g, "");

  // Detect negative values after the symbols are removed, so a sign placed
  // after the symbol (e.g. "€ -50") is found. Parentheses indicate negative
  // in accounting.
  const isNegative =
    cleaned.startsWith("(") ||
    cleaned.startsWith("-") ||
    cleaned.endsWith("-") ||
    (cleaned.endsWith(")") && cleaned.includes("("));

  cleaned = cleaned.replace(/[()]/g, "").replace(/^-|-$/g, "");

  // Determine the decimal separator by looking at the last separator
  // European format uses comma as decimal: 1.234,56
  // US/UK format uses period as decimal: 1,234.56
  const lastComma = cleaned.lastIndexOf(",");
  const lastPeriod = cleaned.lastIndexOf(".");
  const hasComma = lastComma !== -1;
  const hasPeriod = lastPeriod !== -1;

  if (hasComma && hasPeriod) {
    // Both separators present - the one that appears last is the decimal
    if (lastComma > lastPeriod) {
      // European format: comma is the decimal separator
      // Remove periods (thousands separator) and replace comma with period
      cleaned = cleaned.replace(/\./g, "").replace(",", ".");
    } else {
      // US/UK format: period is the decimal separator
      // Remove commas (thousands separator)
      cleaned = cleaned.replace(/,/g, "");
    }
  } else if (hasComma) {
    // Only commas present - could be thousands separator or decimal
    // If there's exactly one comma and 1-2 digits after it, treat as decimal
    const parts = cleaned.split(",");
    if (parts.length === 2 && parts[1].length <= 2) {
      cleaned = cleaned.replace(",", ".");
    } else {
      // Multiple commas or 3+ digits after comma = thousands separator
      cleaned = cleaned.replace(/,/g, "");
    }
  } else if (hasPeriod) {
    // Only periods present - could be thousands separator or decimal
    // If there's exactly one period and 1-2 digits after it, treat as decimal
    const parts = cleaned.split(".");
    if (parts.length === 2 && parts[1].length <= 2) {
      // Already in correct format
    } else {
      // Multiple periods or 3+ digits after period = thousands separator
      cleaned = cleaned.replace(/\./g, "");
    }
  }

  const numericValue = parseFloat(cleaned);

  if (isNaN(numericValue)) {
    return null;
  }

  return isNegative ? -Math.abs(numericValue) : numericValue;
}