src/domain/MikroFormat.ts
import { DateStyle, FormatCurrencyInput } from '../interfaces/index.js';
/**
* @description MikroFormat helps you convert and format between different formats,
* for example to JSON, to various types of dates, to currencies and numbers, or
* different string casings.
*
* @example
* import { MikroFormat } from 'mikroformat';
*
* const mikroformat = new MikroFormat();
*/
export class MikroFormat {
private readonly defaultPrecision = 3;
/**
* @description Formats a number, string, or object into a string.
* Undefined or null values will become empty strings.
*
* @example
* mikroformat.toString(123); // "123"
* mikroformat.toString({ abc: 123, foo: { bar: 'qwerty' } }; // '{"abc":123,"foo":{"bar":"qwerty"}}'
*/
public toString(value: number | string | Record<string, any>) {
if (value === undefined || value === null) return '';
if (typeof value === 'object' && value.constructor.name === 'Object')
return JSON.stringify(value);
return value.toString();
}
/**
* @description Formats a number or string as a whole integer.
*
* @example
* mikroformat.toString(123.123); // 123
*/
public toInteger(value: number | string) {
if (typeof value === 'string') return parseInt(parseInt(value).toFixed(0));
if (typeof value === 'number') return parseInt(value.toFixed(0));
throw new Error(`Unable to convert "${value}" to an integer!`);
}
/**
* @description Formats a number or string as a decimal number.
* You may provide a desired level of precision for the decimals.
*
* @example
* mikroformat.toDecimal(123.129631586528, 8); // 123.12963159
*/
public toDecimal(value: number | string, precision?: number) {
if (typeof value === 'string')
return parseFloat(parseFloat(value).toFixed(precision || this.defaultPrecision));
if (typeof value === 'number')
return parseFloat(value.toFixed(precision || this.defaultPrecision));
throw new Error(`Unable to convert "${value}" to a decimal number!`);
}
/**
* @description Converts a value into a boolean true or false.
*
* @example
* mikroformat.toBoolean('false'); // false
*/
public toBoolean(value: unknown) {
if (value === 'true') return true;
if (value === 'false') return false;
return !!value;
}
/**
* @description Formats a number or string into a percentage representation (string).
* You may provide a desired level of precision for the decimals.
*
* @example
* mikroformat.toPercent(24.29179797432987, 3); // "24.292%"
*/
public toPercent(value: number | string, precision?: number) {
const fixedValue = this.toDecimal(value, precision);
return `${fixedValue}%`;
}
/**
* @description Formats a string into a slugified representation, i.e. `hello-world`.
*
* @example
* mikroformat.toSlug('Hello World'); // "hello-world"
*/
public toSlug(value: number | string) {
return value
.toString()
.toLowerCase()
.replace(/[\s_]+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-');
}
/**
* @description Formats a string into a camel-case representation, i.e. `helloWorld`.
*
* @example
* mikroformat.toCamelCase('hello world'); // "helloWorld"
*/
public toCamelCase(value: string) {
return value.toString().replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, (match, index) => {
if (+match === 0) return '';
return index == 0 ? match.toLowerCase() : match.toUpperCase();
});
}
/**
* @description Formats a string into a snake-case representation, i.e. `hello_world`.
*
* @example
* mikroformat.toSnakeCase('Hello World'); // "hello_world"
*/
public toSnakeCase(value: string) {
return value
.toString()
.toLowerCase()
.replace(/[\s-]+/g, '_')
.replace(/[^a-z0-9_]/g, '')
.replace(/__+/g, '_');
}
/**
* @description Formats a string into a title-case representation, i.e. `Hello World`.
*
* @example
* mikroformat.toTitleCase('hello world'); // "Hello World"
*/
public toTitleCase(value: string) {
return value
.toString()
.toLowerCase()
.replace(/(^|\s)\S/g, (text) => text.toUpperCase());
}
/**
* @description Outputs a currency string that is accurately formatted to the locale and currency provided.
*
* @example
* mikroformat.toCurrency({ value: 24837.731, precision: 8, locale: 'sv-SE', currency: 'EUR' }); // "24 837,731 €"
*/
public toCurrency(input: FormatCurrencyInput) {
const { value, locale, precision } = input;
const fixedValue = (() => {
if (typeof value === 'number' || typeof value === 'bigint') return value;
if (typeof value === 'string') return parseFloat(value);
throw new Error(
`Unable to use convert value "${value}" of type "${typeof value}" to a currency!`
);
})();
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'EUR',
maximumSignificantDigits: precision || this.defaultPrecision
})
.format(fixedValue)
.replaceAll(' ', ' '); // Fixes non-standard spaces
}
/**
* @description Formats an object (JSON) into a string representation.
*
* @example
* mikroformat.toJSON('{"abc":123,"foo":{"bar":"qwerty"}}'); // { abc: 123, foo: { bar: 'qwerty' } }
*/
public toJSON(value: string) {
if (this.isJson(value)) return JSON.parse(value);
console.warn(`Provided value "${value}" is not convertible to a JSON representation!`);
}
private isJson = (str: string): Record<string, unknown> | boolean => {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
};
/**
* @description Converts one type of date into another.
* The available styles are:
* - `date`: A basic `YYYY-MM-DD` format, e.g. `2024-02-29`
* - `iso`: ISO-8601 format, e.g. `2024-03-13T13:02:40.000Z`
* - `unix`: A Unix timestamp, e.g. `1710334960000`
* - `utc`: Universal Time Coordinated (RFC 7231) format, e.g. `Wed, 13 Mar 2024 13:02:40 GMT`
*
* @example
* mikroformat.toDate('2024-03-13T13:02:40.000Z', 'date'); // "2024-03-13"
* mikroformat.toDate('2024-03-13', 'iso'); // "2024-03-13T00:00:00.000Z"
* mikroformat.toDate('Wed, 13 Mar 2024 13:02:40 GMT', 'unix'); // 1710334960000
* mikroformat.toDate(1710334960000, 'utc'); // "Wed, 13 Mar 2024 13:02:40 GMT"
*/
public toDate(value: string | number | Date, style: DateStyle) {
const fn = this.dateFunctions[style];
if (fn) return fn(value);
throw new Error('Missing date function!');
}
private dateFunctions: Record<string, any> = {
date: (value: string) => new Date(value).toISOString().split('T')[0],
iso: (value: string) => new Date(value).toISOString(),
unix: (value: string) => Math.floor(new Date(value).getTime()),
utc: (value: string) => new Date(value).toUTCString()
};
/**
* @description Returns an input as a normalized value, for example, mapping `true` to `Is employee`.
*
* Optionally, the `noMatchHandling` parameter may be set to either `keep` (default) or `drop`:
* `keep` will return the input value if no match is found, while `drop` will return an empty string.
*
* Also optionally, a `replacementValue` may be set. If `noMatchHandling` is set to `keep`, then
* any unmatched input will be replaced with the provided replacement value.
*
* @example
* const schema = {
* 'Is employee': ['true', true, 1, /^(yes|employee)$/],
* 'Contractor': ['false', false, 0, /^(no|consultant)$/],
* };
*
* // Use default noMatchHandling
* mikroformat.toNormalized('yes', schema); // 'Is employee'
* mikroformat.toNormalized(false, schema); // 'Contractor'
*
* // Use explicit noMatchHandling params
* mikroformat.toNormalized(123, schema, 'keep'); // 123
* mikroformat.toNormalized(123, schema, 'drop'); // ''
*
* // Setting a custom replacement value
* mikroformat.toNormalized(123, schema, 'keep', 'Did not match'); // 'Did not match'
*/
public toNormalized(
value: unknown,
schema: Record<string, (string | number | boolean | RegExp)[]>,
noMatchHandling: 'keep' | 'drop' = 'keep',
replacementValue?: string | number | boolean
) {
const strValue = value?.toString() || '';
for (const normalizedValue in schema) {
const patterns = schema[normalizedValue];
for (const pattern of patterns) {
if (pattern instanceof RegExp && pattern.test(strValue)) return normalizedValue;
else if (
new RegExp(pattern.toString()).test(strValue) &&
this.isValidNormalizeType(pattern)
)
return normalizedValue;
}
}
if (noMatchHandling === 'keep') return replacementValue ? replacementValue : value;
return '';
}
private isValidNormalizeType(pattern: unknown) {
return (
typeof pattern === 'string' || typeof pattern === 'number' || typeof pattern === 'boolean'
);
}
}