refactor progress
This commit is contained in:
24
packages/number_utils/src/NumberConstants.tsx
Normal file
24
packages/number_utils/src/NumberConstants.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Locales} from '@fluxer/constants/src/Locales';
|
||||
|
||||
export const DEFAULT_COMPACT_MAX_FRACTION_DIGITS = 1;
|
||||
export const DEFAULT_NUMBER_FALLBACK = 0;
|
||||
export const DEFAULT_NUMBER_LOCALE = Locales.EN_US;
|
||||
43
packages/number_utils/src/NumberFormatterCache.tsx
Normal file
43
packages/number_utils/src/NumberFormatterCache.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const numberFormatterCache = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
export function getNumberFormatter(
|
||||
locale: string,
|
||||
numberFormatOptions: Intl.NumberFormatOptions = {},
|
||||
): Intl.NumberFormat {
|
||||
const cacheKey = buildFormatterCacheKey(locale, numberFormatOptions);
|
||||
const cachedFormatter = numberFormatterCache.get(cacheKey);
|
||||
if (cachedFormatter !== undefined) {
|
||||
return cachedFormatter;
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat(locale, numberFormatOptions);
|
||||
numberFormatterCache.set(cacheKey, formatter);
|
||||
return formatter;
|
||||
}
|
||||
|
||||
function buildFormatterCacheKey(locale: string, numberFormatOptions: Intl.NumberFormatOptions): string {
|
||||
const optionEntries = Object.entries(numberFormatOptions)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.sort(([left], [right]) => left.localeCompare(right));
|
||||
const optionKey = optionEntries.map(([key, value]) => `${key}:${String(value)}`).join('|');
|
||||
return `${locale}|${optionKey}`;
|
||||
}
|
||||
204
packages/number_utils/src/NumberFormatting.tsx
Normal file
204
packages/number_utils/src/NumberFormatting.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_COMPACT_MAX_FRACTION_DIGITS,
|
||||
DEFAULT_NUMBER_FALLBACK,
|
||||
DEFAULT_NUMBER_LOCALE,
|
||||
} from '@fluxer/number_utils/src/NumberConstants';
|
||||
import {getNumberFormatter} from '@fluxer/number_utils/src/NumberFormatterCache';
|
||||
import {parseNumberInput} from '@fluxer/number_utils/src/NumberParsing';
|
||||
import type {
|
||||
BoundCompactNumberFormatOptions,
|
||||
BoundCurrencyNumberFormatOptions,
|
||||
CompactNumberFormatOptions,
|
||||
CurrencyNumberFormatOptions,
|
||||
INumberFormatter,
|
||||
NumberFormatBaseOptions,
|
||||
NumberFormatOptions,
|
||||
NumberFormatterFactoryOptions,
|
||||
NumberInput,
|
||||
} from '@fluxer/number_utils/src/NumberTypes';
|
||||
|
||||
interface ResolvedNumberFormatBaseOptions {
|
||||
locale: string;
|
||||
fallbackValue: number;
|
||||
}
|
||||
|
||||
interface CompactFormatOptionsInput {
|
||||
maximumFractionDigits?: number;
|
||||
minimumFractionDigits?: number;
|
||||
}
|
||||
|
||||
interface CurrencyFormatOptionsInput {
|
||||
currency: string;
|
||||
numberFormatOptions?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>;
|
||||
}
|
||||
|
||||
function resolveBaseOptions(options?: NumberFormatBaseOptions): ResolvedNumberFormatBaseOptions {
|
||||
return {
|
||||
locale: options?.locale ?? DEFAULT_NUMBER_LOCALE,
|
||||
fallbackValue: options?.fallbackValue ?? DEFAULT_NUMBER_FALLBACK,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveNumberFormatOptions(optionsOrLocale: NumberFormatOptions | string | undefined): NumberFormatOptions {
|
||||
if (typeof optionsOrLocale === 'string') {
|
||||
return {locale: optionsOrLocale};
|
||||
}
|
||||
|
||||
return optionsOrLocale ?? {};
|
||||
}
|
||||
|
||||
function resolveCompactFormatOptions(
|
||||
optionsOrLocale: CompactNumberFormatOptions | string | undefined,
|
||||
maximumFractionDigits: number | undefined,
|
||||
): CompactNumberFormatOptions {
|
||||
if (typeof optionsOrLocale === 'string') {
|
||||
return {
|
||||
locale: optionsOrLocale,
|
||||
maximumFractionDigits,
|
||||
};
|
||||
}
|
||||
|
||||
if (optionsOrLocale === undefined) {
|
||||
if (maximumFractionDigits === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {maximumFractionDigits};
|
||||
}
|
||||
|
||||
if (maximumFractionDigits === undefined) {
|
||||
return optionsOrLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
...optionsOrLocale,
|
||||
maximumFractionDigits: optionsOrLocale.maximumFractionDigits ?? maximumFractionDigits,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCurrencyFormatOptions(
|
||||
optionsOrCurrency: CurrencyNumberFormatOptions | string,
|
||||
locale: string | undefined,
|
||||
): CurrencyNumberFormatOptions {
|
||||
if (typeof optionsOrCurrency === 'string') {
|
||||
return {
|
||||
currency: optionsOrCurrency,
|
||||
locale,
|
||||
};
|
||||
}
|
||||
|
||||
return optionsOrCurrency;
|
||||
}
|
||||
|
||||
function formatNumberValue(
|
||||
value: NumberInput,
|
||||
resolvedOptions: ResolvedNumberFormatBaseOptions,
|
||||
numberFormatOptions: Intl.NumberFormatOptions = {},
|
||||
): string {
|
||||
const parsedValue = parseNumberInput(value, resolvedOptions.fallbackValue);
|
||||
return getNumberFormatter(resolvedOptions.locale, numberFormatOptions).format(parsedValue);
|
||||
}
|
||||
|
||||
function buildCompactFormatOptions(options: CompactFormatOptionsInput): Intl.NumberFormatOptions {
|
||||
const numberFormatOptions: Intl.NumberFormatOptions = {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: options.maximumFractionDigits ?? DEFAULT_COMPACT_MAX_FRACTION_DIGITS,
|
||||
};
|
||||
|
||||
if (options.minimumFractionDigits !== undefined) {
|
||||
numberFormatOptions.minimumFractionDigits = options.minimumFractionDigits;
|
||||
}
|
||||
|
||||
return numberFormatOptions;
|
||||
}
|
||||
|
||||
function buildCurrencyFormatOptions(options: CurrencyFormatOptionsInput): Intl.NumberFormatOptions {
|
||||
return {
|
||||
...options.numberFormatOptions,
|
||||
style: 'currency',
|
||||
currency: options.currency,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseNumber(value: NumberInput, options: NumberFormatBaseOptions = {}): number {
|
||||
const resolvedOptions = resolveBaseOptions(options);
|
||||
return parseNumberInput(value, resolvedOptions.fallbackValue);
|
||||
}
|
||||
|
||||
export function formatNumber(value: NumberInput, locale?: string): string;
|
||||
export function formatNumber(value: NumberInput, options?: NumberFormatOptions): string;
|
||||
export function formatNumber(value: NumberInput, optionsOrLocale: NumberFormatOptions | string = {}): string {
|
||||
const options = resolveNumberFormatOptions(optionsOrLocale);
|
||||
const resolvedOptions = resolveBaseOptions(options);
|
||||
return formatNumberValue(value, resolvedOptions, options.numberFormatOptions);
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value: NumberInput, locale?: string, maximumFractionDigits?: number): string;
|
||||
export function formatCompactNumber(value: NumberInput, options?: CompactNumberFormatOptions): string;
|
||||
export function formatCompactNumber(
|
||||
value: NumberInput,
|
||||
optionsOrLocale: CompactNumberFormatOptions | string = {},
|
||||
maximumFractionDigits?: number,
|
||||
): string {
|
||||
const options = resolveCompactFormatOptions(optionsOrLocale, maximumFractionDigits);
|
||||
const resolvedOptions = resolveBaseOptions(options);
|
||||
return formatNumberValue(value, resolvedOptions, buildCompactFormatOptions(options));
|
||||
}
|
||||
|
||||
export function formatCurrency(value: NumberInput, currency: string, locale?: string): string;
|
||||
export function formatCurrency(value: NumberInput, options: CurrencyNumberFormatOptions): string;
|
||||
export function formatCurrency(
|
||||
value: NumberInput,
|
||||
optionsOrCurrency: CurrencyNumberFormatOptions | string,
|
||||
locale?: string,
|
||||
): string {
|
||||
const options = resolveCurrencyFormatOptions(optionsOrCurrency, locale);
|
||||
const resolvedOptions = resolveBaseOptions(options);
|
||||
return formatNumberValue(value, resolvedOptions, buildCurrencyFormatOptions(options));
|
||||
}
|
||||
|
||||
export function createNumberFormatter(options: NumberFormatterFactoryOptions = {}): INumberFormatter {
|
||||
const resolvedOptions = resolveBaseOptions(options);
|
||||
|
||||
function parse(value: NumberInput): number {
|
||||
return parseNumberInput(value, resolvedOptions.fallbackValue);
|
||||
}
|
||||
|
||||
function format(value: NumberInput, numberFormatOptions: Intl.NumberFormatOptions = {}): string {
|
||||
return formatNumberValue(value, resolvedOptions, numberFormatOptions);
|
||||
}
|
||||
|
||||
function formatCompact(value: NumberInput, compactOptions: BoundCompactNumberFormatOptions = {}): string {
|
||||
return formatNumberValue(value, resolvedOptions, buildCompactFormatOptions(compactOptions));
|
||||
}
|
||||
|
||||
function formatCurrency(value: NumberInput, currencyOptions: BoundCurrencyNumberFormatOptions): string {
|
||||
return formatNumberValue(value, resolvedOptions, buildCurrencyFormatOptions(currencyOptions));
|
||||
}
|
||||
|
||||
return {
|
||||
parse,
|
||||
format,
|
||||
formatCompact,
|
||||
formatCurrency,
|
||||
};
|
||||
}
|
||||
44
packages/number_utils/src/NumberParsing.tsx
Normal file
44
packages/number_utils/src/NumberParsing.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {DEFAULT_NUMBER_FALLBACK} from '@fluxer/number_utils/src/NumberConstants';
|
||||
import type {NumberInput} from '@fluxer/number_utils/src/NumberTypes';
|
||||
|
||||
export function parseNumberInput(value: NumberInput, fallbackValue: number = DEFAULT_NUMBER_FALLBACK): number {
|
||||
if (typeof value === 'number') {
|
||||
if (Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmedValue = value.trim();
|
||||
if (trimmedValue === '') {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const parsedValue = Number(trimmedValue);
|
||||
if (Number.isFinite(parsedValue)) {
|
||||
return parsedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
58
packages/number_utils/src/NumberTypes.tsx
Normal file
58
packages/number_utils/src/NumberTypes.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export type NumberInput = number | string | null | undefined;
|
||||
|
||||
export interface NumberFormatBaseOptions {
|
||||
locale?: string;
|
||||
fallbackValue?: number;
|
||||
}
|
||||
|
||||
export interface NumberFormatOptions extends NumberFormatBaseOptions {
|
||||
numberFormatOptions?: Intl.NumberFormatOptions;
|
||||
}
|
||||
|
||||
export interface CompactNumberFormatOptions extends NumberFormatBaseOptions {
|
||||
maximumFractionDigits?: number;
|
||||
minimumFractionDigits?: number;
|
||||
}
|
||||
|
||||
export interface CurrencyNumberFormatOptions extends NumberFormatBaseOptions {
|
||||
currency: string;
|
||||
numberFormatOptions?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>;
|
||||
}
|
||||
|
||||
export interface NumberFormatterFactoryOptions extends NumberFormatBaseOptions {}
|
||||
|
||||
export interface BoundCompactNumberFormatOptions {
|
||||
maximumFractionDigits?: number;
|
||||
minimumFractionDigits?: number;
|
||||
}
|
||||
|
||||
export interface BoundCurrencyNumberFormatOptions {
|
||||
currency: string;
|
||||
numberFormatOptions?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>;
|
||||
}
|
||||
|
||||
export interface INumberFormatter {
|
||||
parse(value: NumberInput): number;
|
||||
format(value: NumberInput, numberFormatOptions?: Intl.NumberFormatOptions): string;
|
||||
formatCompact(value: NumberInput, options?: BoundCompactNumberFormatOptions): string;
|
||||
formatCurrency(value: NumberInput, options: BoundCurrencyNumberFormatOptions): string;
|
||||
}
|
||||
Reference in New Issue
Block a user