shadimage
preskok
uinumber-formatsource: ok · hit

Number Format

A number format component that allows you to format numbers in a variety of ways

npx shadcn add "https://ui-three-mu.vercel.app/r/number-format.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
react-number-format

Registry dependencies

https://ui-three-mu.vercel.app/r/create-ctx.jsonhttps://ui-three-mu.vercel.app/r/locale-context.json

Files4 · source cached 10/14/2026, 1:26:55 PM

registry/preskok/ui/preskok-ui/number-format/number-format.tsx
"use client"

import type { PropsWithChildren } from "react"
import React, { useRef } from "react"
import type { NumericFormatProps } from "react-number-format"
import { NumericFormat } from "react-number-format"

import { LocaleContext } from "@/components/ui/preskok-ui/locale-context/locale-context"
import type { Locale } from "@/components/ui/preskok-ui/locale-context/locale-helpers"
import { CurrencyCodeAndSymbolMap } from "@/components/ui/preskok-ui/locale-context/locale-helpers"
import {
  NumberFormatContext,
  useNumberFormatContext,
} from "@/components/ui/preskok-ui/number-format/number-format-context"

interface NumberFormatPropsUnified extends Omit<
  NumericFormatProps,
  "onChange" | "onPasteCapture" | "onValueChange"
> {
  value: number | string | undefined | null
  isCurrency?: boolean
  onValue?: (value: number | null) => void
  onCustomChangeHandler?: (value: number | null) => void
  onCustomPasteCaptureHandler?: (value: number | null) => void
  onCustomValueChangeHandler?: (values: {
    floatValue?: number
    value: string
  }) => void
}

/**
 * @description NumberFormat component is a unified wrapper around the NumericFormat component from react-number-format. It provides a consistent way to format numbers, handle changes, and paste events. It is advised to
 * use the onValue callback to handle any other state change callbacks, because it abstracts the onChange, onPasteCapture and onValueChange handlers and also
 * provide consistent behavior across different use cases.
 *
 * IMPORTANT: This component abstracts onChange, onPasteCapture and onValueChange handlers, allowing you to handle formatted values easily. Should you
 * want to use custom handlers, you can pass them as props. If no handlers are provided, the component will log a warning in the console and will
 * not process the value changes or pasted values.
 *
 * @param {number | string | undefined | null} value - The value to be formatted and displayed in the input field.
 * @param {boolean} [isCurrency=false] - If true, the component will format the value as a currency. Defaults to false.
 * @param {string} [suffix] - Optional suffix to be appended to the formatted value. If isCurrency is true, it defaults to the Euro currency symbol.
 * @param {(value: number | null) => void} [onValue] - Callback function to handle the formatted value when it changes. IMPORTANT: This will abstract the onChange, onPasteCapture and onValueChange handlers!
 * @param {(value: number | null) => void} [onCustomChangeHandler] - Custom change handler to process the value when it changes.
 * @param {(values: { floatValue?: number; value: string }) => void} [onCustomValueChangeHandler] - Custom value change handler to process the values when they change.
 * @param {(value: number | null) => void} [onCustomPasteCaptureHandler] - Custom paste handler to process the value when pasted.
 *
 * @returns {JSX.Element} - Returns a NumericFormat component with the provided props and handlers.
 */
export const NumberFormat: React.FC<NumberFormatPropsUnified> = ({
  value,
  isCurrency = false,
  suffix,
  onValue,
  onCustomChangeHandler,
  onCustomPasteCaptureHandler,
  ...rest
}) => {
  // store the last valid value to revert to in case of out-of-range values
  const lastValidValueRef = useRef<number | null>(null)
  const {
    withDefaultNumericFormatProps,
    convertStringToNumber,
    isNumberOutOfRange,
  } = useNumberFormatContext()
  const formatSuffix = isCurrency
    ? suffix || CurrencyCodeAndSymbolMap.EUR
    : suffix

  // Unified handler for value changes
  const _handleValueChange = (values: {
    floatValue?: number
    value: string
  }) => {
    const { floatValue } = values
    let finalFloatValue: number | null = floatValue ?? null

    if (!isNumberOutOfRange(floatValue ?? null)) {
      // no toast thrown, because it should be picked up by the onChange handler already

      lastValidValueRef.current = floatValue ?? null
    } else {
      finalFloatValue = lastValidValueRef.current // Revert to last valid value
    }

    if (onValue) {
      onValue(finalFloatValue)
    } else if (onCustomChangeHandler) {
      onCustomChangeHandler(finalFloatValue ?? null)
    } else {
      console.warn(
        "No onValue or onCustomChangeHandler provided, value will not be processed."
      )
    }
  }

  // Unified handler for manual input changes (fallback)
  const _handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const val = e.target.value

    let convertToNumber = convertStringToNumber({
      value: val,
    })

    if (!isNumberOutOfRange(convertToNumber)) {
      lastValidValueRef.current = convertToNumber
    } else {
      // consider throwing a toast or letting the user know another way
      convertToNumber = lastValidValueRef.current ?? null // Revert to last valid value
    }

    if (onValue) {
      onValue(convertToNumber)
    } else if (onCustomChangeHandler) {
      onCustomChangeHandler(convertToNumber)
    } else {
      console.warn(
        "No onValue or onCustomChangeHandler provided, value will not be processed."
      )
    }
  }

  // Unified handler for paste
  const _handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
    e.preventDefault()
    const pastedValue = e.clipboardData.getData("text")
    let tempPastedValue: string | null = null
    if (pastedValue) {
      tempPastedValue = pastedValue.replace(/[^0-9.,]/g, "")
      const lastDotIndex = tempPastedValue.lastIndexOf(".")
      const lastCommaIndex = tempPastedValue.lastIndexOf(",")
      const lastSeparatorIndex = Math.max(lastDotIndex, lastCommaIndex)
      if (lastSeparatorIndex !== -1) {
        const integerPart = tempPastedValue
          .substring(0, lastSeparatorIndex)
          .replace(/[,.]/g, "")
        const decimalPart = tempPastedValue
          .substring(lastSeparatorIndex + 1)
          .replace(/[,.]/g, "")
        tempPastedValue = `${integerPart}.${decimalPart}`
      } else {
        tempPastedValue = tempPastedValue.replace(/[^0-9]/g, "")
      }
    }

    let checkedPastedNumber =
      tempPastedValue && !isNaN(parseFloat(tempPastedValue))
        ? parseFloat(tempPastedValue)
        : null

    if (!isNumberOutOfRange(checkedPastedNumber)) {
      lastValidValueRef.current = checkedPastedNumber
    } else {
      // consider throwing a toast or letting the user know another way
      checkedPastedNumber = lastValidValueRef.current // Revert to last valid value
    }

    if (onValue) {
      onValue(checkedPastedNumber)
    } else if (onCustomPasteCaptureHandler) {
      onCustomPasteCaptureHandler(checkedPastedNumber)
    } else {
      console.warn(
        "No onValue or onCustomPasteCaptureHandler provided, pasted value will not be processed."
      )
    }
  }

  return (
    <NumericFormat
      value={value}
      suffix={formatSuffix}
      onChange={_handleChange}
      onValueChange={_handleValueChange}
      onPasteCapture={_handlePaste}
      {...withDefaultNumericFormatProps}
      {...rest}
    />
  )
}

export function NumberFormatProvider({ children }: PropsWithChildren) {
  return (
    <LocaleContext>
      <NumberFormatContext>{children}</NumberFormatContext>
    </LocaleContext>
  )
}

interface NumberTextProps {
  value: number
  className?: string
  intlOptions?: Intl.NumberFormatOptions
  locale?: Locale
  invalidNumberText?: string
}

export function NumberText({
  value,
  className,
  intlOptions,
  locale,
  invalidNumberText,
}: NumberTextProps) {
  const { formatNumberRaw } = useNumberFormatContext()
  const res = formatNumberRaw(value, { locale, intlOptions, invalidNumberText })
  return (
    <span className={className}>
      {res.value ?? res.invalidNumberText ?? ""}
    </span>
  )
}

export const useNumberFormat = useNumberFormatContext
registry/preskok/ui/preskok-ui/number-format/number-format-context.tsx
"use client"

import type { PropsWithChildren } from "react"
import React, { useCallback, useMemo } from "react"
import type { NumericFormatProps } from "react-number-format"

import { createCtx } from "@/lib/create-ctx"
import { useLocaleContext } from "@/components/ui/preskok-ui/locale-context/locale-context"
import { Locale } from "@/components/ui/preskok-ui/locale-context/locale-helpers"
import {
  convertLocaleToNumericProps,
  formatNumberToString,
} from "@/components/ui/preskok-ui/number-format/number-format-helpers"

const MAXIMUM_NUMBER_OF_DIGITS_ALLOWED = 12

const DEFAULT_NUMBER_OF_DECIMALS = 2

const DEFAULT_NUMERIC_FORMAT_OPTIONS: NumericFormatProps = {
  displayType: "text",
  thousandSeparator: ".",
  decimalSeparator: ",",
  allowNegative: true,
  decimalScale: DEFAULT_NUMBER_OF_DECIMALS,
}

const FRENCH_NUMERIC_FORMAT_OPTIONS: NumericFormatProps = {
  ...DEFAULT_NUMERIC_FORMAT_OPTIONS,
  thousandSeparator: " ",
  decimalSeparator: ",",
}
const ENGLISH_NUMERIC_FORMAT_OPTIONS: NumericFormatProps = {
  ...DEFAULT_NUMERIC_FORMAT_OPTIONS,
  thousandSeparator: ",",
  decimalSeparator: ".",
}

const SLOVENE_NUMERIC_FORMAT_OPTIONS: NumericFormatProps = {
  ...DEFAULT_NUMERIC_FORMAT_OPTIONS,
  thousandSeparator: ".",
  decimalSeparator: ",",
}

const DEFAULT_LOCALE = Locale.SL

interface ConvertStringToNumberBasedOnLocaleProps {
  value?: string | null
  forcedLocale?: Locale
}

interface NumberFormatContextProviderProps {
  formatNumberRaw: FormatNumberToStringFnSignature
  withDefaultNumericFormatProps: NumericFormatProps
  convertCustomLocaleToNumericProps: ConvertCustomLocaleToNumericPropsFnSignature
  convertStringToNumber: ({
    value,
    forcedLocale,
  }: ConvertStringToNumberBasedOnLocaleProps) => number | null
  isNumberOutOfRange: (value: number | null) => boolean
}

export const [useNumberFormatContext, NumberFormatContextProvider] =
  createCtx<NumberFormatContextProviderProps>()

export const NumberFormatContext: React.FC<PropsWithChildren> = ({
  children,
}) => {
  const { globalLocale } = useLocaleContext()

  /**
   * @description evaluateNumericFormatDefaults will return specific defaults for a given locale.
   */
  const evaluateNumericFormatDefaults = () => {
    switch (globalLocale) {
      case "sl":
      case "de":
      case "it":
      case "hr":
        return {
          ...SLOVENE_NUMERIC_FORMAT_OPTIONS,
        }
      case "fr":
        return {
          ...FRENCH_NUMERIC_FORMAT_OPTIONS,
        }
      case "en":
        return {
          ...ENGLISH_NUMERIC_FORMAT_OPTIONS,
        }
      default:
        return {
          ...SLOVENE_NUMERIC_FORMAT_OPTIONS,
        }
    }
  }

  /**
   * @description formatNumberRaw will format a number to a string - this is just a wrapper around the helper function formatNumberToString. If the number is not valid, the function will return the default invalidNumberText string. Please
   * note this function is meant to display numbers to the end user and will take locale and Intl.NumberFormatOptions into account (if any passed).
   *
   * Locale will default to 'sl' if not passed, otherwise the global locale will be taken into the account.
   *
   * In case the number is not valid (not finite) then a default invalidNumberText will be returned. Developers can further customize any of this via options object (not required).
   *
   * NOTE: This will achieve similar result to what withDefaultNumericFormatProps will forward to the react-number-format component, but this function will return a string instead of a component.
   *
   * @param value -> The number value to be formatted
   * @param options -> The options object (FormatNumberToStringOptions)
   * @returns {string}
   */
  const formatNumberRaw: FormatNumberToStringFnSignature = useCallback(
    (
      value,
      // oxlint-disable-next-line typescript/no-unnecessary-condition
      options = { locale: globalLocale || DEFAULT_LOCALE }
    ): FormatNumberToStringReturnType => {
      return formatNumberToString(value, options)
    },
    [globalLocale]
  )

  /**
   * @description convertCustomLocaleToNumericProps is a memoized function that will convert a custom locale to a set of props for the react-number-format component. The goal
   * of this function is to allow developers to get default props based on custom locale (otherwise the NumberFormat component will take global locale into account when using the withDefaultNumericFormatProps).
   *
   * @param {Locale} locale -> The locale to convert to props
   * @returns {NumericFormatProps}
   */
  const convertCustomLocaleToNumericProps = useCallback(
    (locale: Locale): NumericFormatProps => {
      return convertLocaleToNumericProps(locale)
    },
    []
  )

  /**
   * @description withDefaultNumericFormatProps is a memoized function that returns a default set of props for the react-number-format component. It will take
   * current locale into account and return the appropriate props.
   *
   * EXAMPLE usage:
   * <NumberFormat value={123445.44433} />
   *
   * The usage above will spread the default props into the component, but will also allow you to override them with your own props should you need to.
   *
   * VERY IMPORTANT: The 'withDefaultNumericFormatProps' will take global locale into account and cannot be overwritten. If you need to overwrite the locale you will need to provide a different
   * set of props to the component! Please use the `convertCustomLocaleToNumericProps` function available on context.
   *
   * @returns default props for react-number-format component
   */
  const withDefaultNumericFormatProps = evaluateNumericFormatDefaults()

  /**
   * @description This function will convert a string to a number based on the locale. It will replace the thousands separator with an empty string and the decimal separator with a dot. The
   * locales are mapped against our internal settings for separators. This will work in conjunction with - but is not limited by - the NumberFormat component and will allow you to convert a string value to a number based on the locale.
   *
   * @param {string | null | undefined} value - The string value to be converted to a number based on the locale.
   * @param {Locale} [forcedLocale] - The locale to use for conversion. If not provided, the global locale will be used.
   * @returns {number | null} - Returns the parsed number or null if the input is invalid.
   */
  const convertStringToNumberBasedOnLocale = useCallback(
    ({
      value,
      // oxlint-disable-next-line typescript/no-unnecessary-condition
      forcedLocale = globalLocale || DEFAULT_LOCALE,
    }: ConvertStringToNumberBasedOnLocaleProps): number | null => {
      if (typeof value !== "string" || value.trim() === "") {
        return null
      }

      const cleanedValue = value.replace(/[^0-9.,]/g, "")

      let normalizedValue = cleanedValue

      /**
       * Convert the forcedLocale (or the globalLocale | DEFAULT_LOCALE) to numeric props to get the thousand and decimal separators as per our internal settings.
       * This will allow us to normalize the value based on the locale.
       */
      const { thousandSeparator, decimalSeparator } =
        convertCustomLocaleToNumericProps(forcedLocale)

      /**
       * Replace the thousands separator with an empty string and the decimal separator with a dot
       */
      normalizedValue =
        cleanedValue
          .replace(new RegExp(`\\${thousandSeparator}`, "g"), "")
          .replace(new RegExp(`\\${decimalSeparator}`, "g"), ".") || ""
      const parsedValue = parseFloat(normalizedValue)
      if (isNaN(parsedValue) || !isFinite(parsedValue)) {
        return null
      }

      return parsedValue
    },
    [convertCustomLocaleToNumericProps, globalLocale]
  )

  const isNumberOutOfRange = useCallback((value: number | null) => {
    // We need to check if the value has no more than MAXIMUM_NUMBER_OF_DIGITS_ALLOWED digits. Please note that this limit only applies to the
    // integer part of the number, not the decimal part.

    // oxlint-disable-next-line typescript/no-unnecessary-condition
    if (value === null || value === undefined) {
      return false
    }

    const valueString = value.toString()
    const integerPart = valueString.split(".")[0] // Get the integer part of the number
    return integerPart.length > MAXIMUM_NUMBER_OF_DIGITS_ALLOWED
  }, [])

  const memoizedContextProps = useMemo(
    () => ({
      formatNumberRaw,
      withDefaultNumericFormatProps,
      convertCustomLocaleToNumericProps,
      convertStringToNumber: convertStringToNumberBasedOnLocale,
      isNumberOutOfRange,
    }),
    [
      formatNumberRaw,
      withDefaultNumericFormatProps,
      convertCustomLocaleToNumericProps,
      convertStringToNumberBasedOnLocale,
      isNumberOutOfRange,
    ]
  )

  return (
    <NumberFormatContextProvider value={memoizedContextProps}>
      {children}
    </NumberFormatContextProvider>
  )
}

export type IsNumberValidAllowedTypes = string | number | null | undefined

export interface ProcessNumberReturnObj {
  isNumberValid: boolean
  value: number | null
}

export interface FormatNumberToStringOptions {
  locale?: Locale
  intlOptions?: Intl.NumberFormatOptions
  invalidNumberText?: string
}

export type FormatNumberToStringFnSignature = (
  value: number,
  options?: FormatNumberToStringOptions
) => FormatNumberToStringReturnType

export type ConvertCustomLocaleToNumericPropsFnSignature = (
  locale: Locale
) => NumericFormatProps

export interface FormatNumberToStringReturnType extends Pick<
  FormatNumberToStringOptions,
  "invalidNumberText"
> {
  isValid: boolean
  value: string | null
}
registry/preskok/ui/preskok-ui/number-format/number-format-helpers.ts
import type { NumericFormatProps } from "react-number-format"

import type {
  Locale,
  LocaleToBcp47Map,
} from "@/components/ui/preskok-ui/locale-context/locale-helpers"
import {
  DEFAULT_INVALID_NUMBER_TEXT,
  DEFAULT_LOCALE,
  DEFAULT_NUMBER_OF_DECIMALS,
  DEFAULT_NUMERIC_FORMAT_OPTIONS,
  LOCALE_TO_BCP_47_MAP,
  LOCALE_TO_NUMERIC_FORMAT_OPTIONS_MAP,
} from "@/components/ui/preskok-ui/locale-context/locale-helpers"

/**
 * isNumberValid will check whether the passed value is a number. It leverages the Number.isFinite method which is the
 * most consistent way to check whether a value is a number. Edge cases are handled correctly, thus NaN, undefined, null
 * and Infinity are not considered numbers.
 *
 * @param {IsNumberValidAllowedTypes} value -> The value to be checked
 * @returns {boolean}
 */
export const isNumberValid = (value: IsNumberValidAllowedTypes): boolean => {
  return Number.isFinite(value)
}

/**
 * convertStringToNumber will attempt to convert a string to a number. If the conversion is successful, the function will return the converted number. If the conversion is not
 * successful, the function will return null.
 *
 * @param {string} value -> string value to be converted to a number
 * @returns {number | null}
 */
export const convertStringToNumber = (value: string): number | null => {
  const convertedStringValue = Number(value)

  return isNumberValid(convertedStringValue) ? convertedStringValue : null
}

/**
 * numberIsInteger will check whether the passed number is an integer and not a float.
 *
 * @param {number} value -> The value to be checked
 * @returns {boolean}
 */
export const numberIsInteger = (value: number): boolean => {
  return Number.isInteger(value)
}

/**
 * numberIsPositive will check whether the passed number is positive.
 *
 * @param {number} value -> The value to be checked
 * @returns {boolean}
 */
export const numberIsPositive = (value: number): boolean => {
  return value >= 0
}

/**
 * @description convertLocaleToBcp47 will convert a locale to BCP 47 format. If the locale is not found in the LOCALE_TO_BCP_47_MAP, the function will return an empty string.
 *
 * @param {Locale} locale -> The locale to be converted to BCP 47 format
 * @returns {string} - either bcp 47 locale or empty string
 */
export const convertLocaleToBcp47 = (locale: Locale): LocaleToBcp47Map => {
  const bcp47Map = LOCALE_TO_BCP_47_MAP

  return bcp47Map[locale]
}

/**
 * @description convertLocaleToNumericProps will convert a locale to numeric format options. If the locale is not found in the LOCALE_TO_NUMERIC_FORMAT_OPTIONS_MAP, the function will return
 * DEFAULT_NUMERIC_FORMAT_OPTIONS.
 *
 * @param {`${Locale}`} locale -> The locale to be converted to numeric format options
 * @returns {NumericFormatProps}
 */
export const convertLocaleToNumericProps = (
  locale: Locale
): NumericFormatProps => {
  return (
    // oxlint-disable-next-line typescript/no-unnecessary-condition
    LOCALE_TO_NUMERIC_FORMAT_OPTIONS_MAP[locale] ??
    DEFAULT_NUMERIC_FORMAT_OPTIONS
  )
}

/**
 * processId will attempt to convert a string to a number and then check whether the number is valid and positive. This function is used to process the id parameter anywhere
 * in the code. Explanation: for historic reasons we are oftentimes receiving a string as an id parameter. This function will attempt to convert the string to a number and then
 * evaluate whether the number is valid, positive and is not a float, thus reflecting the fact that we are dealing with an id. If the id is valid, the function will return an
 * object containing the value of the id and a boolean indicating that the id is valid.
 *
 * @param {IsNumberValidAllowedTypes} value -> The value to be processed
 * @returns {ProcessNumberReturnObj}
 */
export const processId = (
  value: IsNumberValidAllowedTypes
): ProcessNumberReturnObj => {
  const processNumberResult: ProcessNumberReturnObj = {
    isNumberValid: false,
    value: null,
  }

  /**
   * First we attempt to convert string to a number. If the value is not a string, we will simply use the value as is.
   */
  const attemptConversion =
    typeof value === "string" ? convertStringToNumber(value) : value

  /**
   * We check whether the value is a number and whether it is positive and an integer. If all of these conditions are met, we will return the value as a number.
   *
   * IMPORTANT: we are casting the passed value as number, because in theory the 'isNumberValid' will only be truthy if the value is an actual number. However, TypeScript
   * is not able to infer that the value is a number, thus we need to cast it as number in the numberIsInteger and numberIsPositive functions. If another check would be added then
   * extra caution needs to be exercised as it might break the logic!
   */
  const isIdValid =
    isNumberValid(attemptConversion) &&
    numberIsInteger(attemptConversion!) &&
    numberIsPositive(attemptConversion!)

  if (isIdValid) {
    processNumberResult.isNumberValid = true
    processNumberResult.value = attemptConversion!
  }

  return processNumberResult
}

/**
 * @description formatNumberToString will format a number to a string. If the number is not valid, the function will return the default invalidNumberText string. Please
 * note this function is meant to display numbers to the end user and will take locale and Intl.NumberFormatOptions into account (if any passed).
 *
 * Locale will default to 'sl' if not passed.
 *
 * In case the number is not valid (not finite) then a default invalidNumberText will be returned. Developers can further customize any of this via options object (not required).
 *
 * @param value -> The number value to be formatted
 * @param options -> The options object (FormatNumberToStringOptions)
 * @returns {FormatNumberToStringReturnType} -> an object containing data regarding validity of number and the formatted number as string. There is also an option to retrieve a fallback text / value.
 */
export const formatNumberToString: FormatNumberToStringFnSignature = (
  value: number,
  options?: FormatNumberToStringOptions
): FormatNumberToStringReturnType => {
  const isValid = isNumberValid(value)

  /**
   * The end result of the function. We will return this object.
   */
  const formattedNumberResult: FormatNumberToStringReturnType = {
    value: null,
    isValid,
  }

  /**
   * We will merge the passed options with the default options. This will ensure that the developer can override any of the default options.
   */
  const defaultOptionsMerge: FormatNumberToStringOptions = {
    locale: DEFAULT_LOCALE,
    invalidNumberText: DEFAULT_INVALID_NUMBER_TEXT,
    intlOptions: {
      minimumFractionDigits: DEFAULT_NUMBER_OF_DECIMALS,
      maximumFractionDigits: DEFAULT_NUMBER_OF_DECIMALS,
    },
    ...options,
  }

  const { locale, intlOptions, invalidNumberText } = defaultOptionsMerge

  /**
   * Copy the invalid number text to the output object (in case developer provided custom version)
   */
  if (invalidNumberText) {
    formattedNumberResult.invalidNumberText = invalidNumberText
  }

  /**
   * Perform formatting only if the number is valid and locale is provided.
   */
  if (isValid && locale) {
    const mappedLocale = convertLocaleToBcp47(locale)

    if (mappedLocale) {
      formattedNumberResult.value = value.toLocaleString(
        mappedLocale,
        intlOptions
      )
    }
  }

  return formattedNumberResult
}

export type IsNumberValidAllowedTypes = string | number | null | undefined

export interface ProcessNumberReturnObj {
  isNumberValid: boolean
  value: number | null
}

export interface FormatNumberToStringOptions {
  locale?: Locale
  intlOptions?: Intl.NumberFormatOptions
  invalidNumberText?: string
}

export type FormatNumberToStringFnSignature = (
  value: number,
  options?: FormatNumberToStringOptions
) => FormatNumberToStringReturnType

export interface FormatNumberToStringReturnType extends Pick<
  FormatNumberToStringOptions,
  "invalidNumberText"
> {
  isValid: boolean
  value: string | null
}
registry/preskok/ui/preskok-ui/number-format/currency-helpers.ts
import { CurrencyCodeAndSymbolMap } from "../locale-context/locale-helpers"

type CurrencyCodeOrSymbolExpectedOutputOptions =
  | "currency_code"
  | "currency_symbol"

interface GetCurrencyCodeOrSymbolParams {
  inputValue: string
  expectedOutput?: CurrencyCodeOrSymbolExpectedOutputOptions
}

/**
 * getCurrencyCodeOrSymbol is a helper function to get currency code or symbol from a string value. If no match is found
 * we return an empty string.
 *
 * @param {string} inputValue -> string value that might be a currency code or symbol.
 * @param {'currency_code' | 'currency_symbol'} expectedOutput -> optional expected output type (default: 'currency_code').
 * @returns {string} - currency code or symbol or an empty string if no match is found
 */
export const getCurrencyCodeOrSymbol = ({
  inputValue,
  expectedOutput = "currency_code",
}: GetCurrencyCodeOrSymbolParams): string => {
  let currencyCodeOrSymbolStringValue = ""
  const trimmedAndLowerCaseInput = inputValue.trim().toLowerCase()

  const currencyCodeOrSymbolLookup = Object.keys(CurrencyCodeAndSymbolMap).find(
    (key) =>
      key.toLowerCase() === trimmedAndLowerCaseInput ||
      CurrencyCodeAndSymbolMap[
        key as keyof typeof CurrencyCodeAndSymbolMap
      ].toLowerCase() === trimmedAndLowerCaseInput
  )

  if (currencyCodeOrSymbolLookup) {
    if (expectedOutput === "currency_code") {
      currencyCodeOrSymbolStringValue = currencyCodeOrSymbolLookup
    } else {
      currencyCodeOrSymbolStringValue =
        CurrencyCodeAndSymbolMap[
          currencyCodeOrSymbolLookup as keyof typeof CurrencyCodeAndSymbolMap
        ]
    }
  }

  return currencyCodeOrSymbolStringValue
}

More from preskok

Browse all components in this library →