/** * utils for working with numbers * * @module */ import { type Codec, decodingError } from "./Codec.js"; import * as Either from "effect/Either"; import { iife } from "./Function.js"; import * as Option from "./Option.js"; export const tryParse = (value: unknown) => { const number = Number(value); return Number.isNaN(number) ? Option.none : Option.some(number); }; export const eq = (a: number, b: number) => a === b; export const gt = (a: number, b: number) => a > b; export const gte = (a: number, b: number) => a >= b; export const lt = (a: number, b: number) => a < b; export const lte = (a: number, b: number) => a <= b; /** * converts between string and number. Invalid values are treated as a right undefined */ export const contentAttribute__maybeNumber: Codec = { encode: (value: number | undefined) => Either.right(value?.toString() ?? ""), decode: (value: unknown) => value == null || value === "" ? Either.right(undefined) : typeof value === "number" ? Either.right(value) : typeof value === "string" ? iife(() => { const asNumber = Number(value); return Number.isNaN(asNumber) ? Either.right(undefined) : Either.right(asNumber); }) : Either.left(decodingError(value)), }; /** * converts between a string and a number. NaN values also produce a left */ export const contentAttribute__number: Codec = { encode: (value: number) => Number.isNaN(value) ? Either.left(new Error("cannot encode NaN into a number")) : Either.right(value.toString()), decode: (value) => value == null || value === "" ? Either.left(new Error("empty value cannot be interpreted as number")) : typeof value === "number" ? Number.isNaN(value) ? Either.left(new Error("NaN is not a number for this codec")) : Either.right(value) : typeof value === "string" ? iife(() => { const numeric = Number(value); return Number.isNaN(numeric) ? Either.left(new Error(`'${value}' is not a number`)) : Either.right(numeric); }) : Either.left(decodingError(value)), }; /** * converts between a locale aware number format (without grouping) and a number */ export const localeContentAttribute__number = ( forLocale: string, ): Codec => { const formatter = new Intl.NumberFormat(forLocale, { useGrouping: false, }); const decimal = formatter .formatToParts(1.1) .find(({ type }) => type === "decimal")!.value; const illegalChars = new RegExp(`[^\\d${decimal}]`); return { encode: (value) => Either.right(value?.toString().replace(".", decimal) ?? ""), decode: (value) => value == null ? Either.right(undefined) : typeof value === "number" ? Either.right(value) : typeof value === "string" ? iife(() => { if ( illegalChars.test(value) || (value.indexOf(decimal) < value.length - 1 && value.endsWith(decimal)) ) { return Either.left( new Error(`${value} contained illegal characters`), ); } const parsed = value === "" ? undefined : Number(value.replace(decimal, ".").replace(/\.$/, "")); return Number.isNaN(parsed) ? Either.left( new Error(`${value} could not be parsed as number`), ) : Either.right(parsed); }) : Either.left(decodingError(value)), }; };