Number Formatter

PreviousNext

A utility function for formatting numbers and currencies.

Docs
aevrlib

Preview

Loading preview…
registry/utils/number-formatter.ts
// ./registry/utils/number-formatter.ts

import { logger } from "@untools/logger";
import { getParamByParam } from "iso-country-currency";

interface FormatCurrencyOptions {
  currency?: string;
  locale?: string;
  minimumFractionDigits?: number;
  maximumFractionDigits?: number;
  display?: "code" | "symbol" | "both";
  symbolFirst?: boolean;
}

export function formatCurrency(
  value: number,
  options: FormatCurrencyOptions = {}
): string {
  const {
    currency = "USD",
    locale = "en-US",
    minimumFractionDigits = 2,
    maximumFractionDigits = 5,
    display = "symbol",
    symbolFirst = true,
  } = options;

  try {
    // Format the number first
    const formattedNumber = new Intl.NumberFormat(locale, {
      style: "decimal",
      minimumFractionDigits,
      maximumFractionDigits,
    }).format(value);

    // Get symbol if needed
    let symbol = "";
    if (display === "symbol" || display === "both") {
      try {
        const foundSymbol = getParamByParam("currency", currency, "symbol");
        if (foundSymbol) {
          symbol = foundSymbol;
        }
      } catch {
        // If symbol lookup fails, fall back to code-only display
        return `${currency} ${formattedNumber}`;
      }
    }

    // Return formatted string based on display option
    switch (display) {
      case "symbol":
        return symbol
          ? `${symbol}${formattedNumber}`
          : `${currency} ${formattedNumber}`;
      case "both":
        if (symbol) {
          return symbolFirst
            ? `${symbol} ${formattedNumber} (${currency})`
            : `${currency} ${formattedNumber} (${symbol})`;
        }
        return `${currency} ${formattedNumber}`;
      case "code":
      default:
        return `${currency} ${formattedNumber}`;
    }
  } catch (error) {
    logger.error("🚫 Something went wrong while formatting currency:", error);
    // Ultimate fallback
    return `${currency} ${value.toFixed(minimumFractionDigits)}`;
  }
}

export function formatNumber(
  value: number,
  options: FormatCurrencyOptions = {}
): string {
  const {
    locale = "en-US",
    minimumFractionDigits = 2,
    maximumFractionDigits = 5,
  } = options;

  try {
    // Format the number first
    const formattedNumber = new Intl.NumberFormat(locale, {
      style: "decimal",
      minimumFractionDigits,
      maximumFractionDigits,
    }).format(value);

    return formattedNumber;
  } catch (error) {
    logger.error("🚫 Something went wrong while formatting number:", error);
    // Ultimate fallback
    return value.toFixed(minimumFractionDigits);
  }
}

export interface FormatOptions {
  mask?: boolean;
  maskChar?: string;
  maskCount?: number;
  spacing?: number;
  separator?: string;
  customPattern?: string | null;
}

export function formatCardNumber(
  input: string | number,
  options: FormatOptions = {}
): string {
  const {
    mask = false,
    maskChar = "*",
    maskCount = 12,
    spacing = 4,
    separator = " ",
    customPattern = null,
  } = options;

  // Convert input to string and remove any existing separators
  const cleanInput = String(input).replace(/\D/g, "");

  // Handle masking (for card numbers)
  if (mask) {
    let masked: string;

    if (cleanInput.length <= 4) {
      // For short inputs (like "3456"), create full card format with masking
      masked = maskChar.repeat(maskCount) + cleanInput.padStart(4, "0");
    } else {
      // For longer inputs, take last 4 digits and mask the rest
      const lastFour = cleanInput.slice(-4);
      masked = maskChar.repeat(maskCount) + lastFour;
    }

    // Apply formatting to masked string
    return applyFormatting(masked, spacing, separator, customPattern);
  }

  // Apply formatting to original input
  return applyFormatting(cleanInput, spacing, separator, customPattern);
}

function applyFormatting(
  str: string,
  spacing: number,
  separator: string,
  customPattern: string | null
): string {
  if (customPattern) {
    // Custom pattern formatting (e.g., "XXXX-XXXX-XXXX-XXXX")
    let result = "";
    let strIndex = 0;

    for (let i = 0; i < customPattern.length && strIndex < str.length; i++) {
      if (customPattern[i] === "X") {
        result += str[strIndex++];
      } else {
        result += customPattern[i];
      }
    }

    // Add remaining digits if any
    if (strIndex < str.length) {
      result += str.slice(strIndex);
    }

    return result;
  }

  // Default spacing formatting
  return str
    .replace(new RegExp(`(.{${spacing}})`, "g"), `$1${separator}`)
    .trim();
}

// // Usage examples:

// // Basic masking (12 asterisks + last 4 digits)
// console.log(formatCardNumber("1234567890123456", { mask: true }));
// // Output: "**** **** **** 3456"

// // Custom mask character and count
// console.log(
//   formatCardNumber("1234567890123456", {
//     mask: true,
//     maskChar: "#",
//     maskCount: 8,
//   }),
// );
// // Output: "#### #### 3456"

// // Format without masking (default: space every 4 digits)
// console.log(formatCardNumber("1234567890123456"));
// // Output: "1234 5678 9012 3456"

// // Custom spacing
// console.log(formatCardNumber("123456789", { spacing: 3, separator: "-" }));
// // Output: "123-456-789"

// // Custom pattern
// console.log(
//   formatCardNumber("1234567890123456", {
//     customPattern: "XXXX-XXXX-XXXX-XXXX",
//   }),
// );
// // Output: "1234-5678-9012-3456"

// // Masked with custom pattern
// console.log(
//   formatCardNumber("1234567890123456", {
//     mask: true,
//     customPattern: "XXXX-XXXX-XXXX-XXXX",
//   }),
// );
// // Output: "****-****-****-3456"

Installation

npx shadcn@latest add @aevr/number-formatter

Usage

import { NumberFormatter } from "@/lib/number-formatter"
NumberFormatter()