137 lines
5.0 KiB
TypeScript
137 lines
5.0 KiB
TypeScript
import { HassEntity, HassEntityAttributeBase } from 'home-assistant-js-websocket';
|
|
import { EntityRegistryDisplayEntry } from '../types/homeassistant';
|
|
import { FrontendLocaleData, NumberFormat } from '../types/translation';
|
|
|
|
// Logic based on https://en.wikipedia.org/wiki/Percent_sign#Form_and_spacing
|
|
export const blankBeforePercent = (localeOptions: FrontendLocaleData): string => {
|
|
switch (localeOptions.language) {
|
|
case 'cz':
|
|
case 'de':
|
|
case 'fi':
|
|
case 'fr':
|
|
case 'sk':
|
|
case 'sv':
|
|
return ' ';
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
export const round = (value: number, precision = 2): number => Math.round(value * 10 ** precision) / 10 ** precision;
|
|
|
|
/**
|
|
* Returns true if the entity is considered numeric based on the attributes it has
|
|
* @param stateObj The entity state object
|
|
*/
|
|
export const isNumericState = (stateObj: HassEntity): boolean => isNumericFromAttributes(stateObj.attributes);
|
|
|
|
export const isNumericFromAttributes = (attributes: HassEntityAttributeBase): boolean =>
|
|
!!attributes.unit_of_measurement || !!attributes.state_class;
|
|
|
|
export const numberFormatToLocale = (localeOptions: FrontendLocaleData): string | string[] | undefined => {
|
|
switch (localeOptions.number_format) {
|
|
case NumberFormat.comma_decimal:
|
|
return ['en-US', 'en']; // Use United States with fallback to English formatting 1,234,567.89
|
|
case NumberFormat.decimal_comma:
|
|
return ['de', 'es', 'it']; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
|
|
case NumberFormat.space_comma:
|
|
return ['fr', 'sv', 'cs']; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
|
|
case NumberFormat.system:
|
|
return undefined;
|
|
default:
|
|
return localeOptions.language;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
|
*
|
|
* @param num The number to format
|
|
* @param localeOptions The user-selected language and formatting, from `hass.locale`
|
|
* @param options Intl.NumberFormatOptions to use
|
|
*/
|
|
export const formatNumber = (
|
|
num: string | number,
|
|
localeOptions?: FrontendLocaleData,
|
|
options?: Intl.NumberFormatOptions,
|
|
): string => {
|
|
const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined;
|
|
|
|
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
|
Number.isNaN =
|
|
Number.isNaN ||
|
|
function isNaN(input) {
|
|
return typeof input === 'number' && isNaN(input);
|
|
};
|
|
|
|
if (localeOptions?.number_format !== NumberFormat.none && !Number.isNaN(Number(num)) && Intl) {
|
|
try {
|
|
return new Intl.NumberFormat(locale, getDefaultFormatOptions(num, options)).format(Number(num));
|
|
} catch (err: any) {
|
|
// Don't fail when using "TEST" language
|
|
// eslint-disable-next-line no-console
|
|
console.error(err);
|
|
return new Intl.NumberFormat(undefined, getDefaultFormatOptions(num, options)).format(Number(num));
|
|
}
|
|
}
|
|
if (typeof num === 'string') {
|
|
return num;
|
|
}
|
|
return `${round(num, options?.maximumFractionDigits).toString()}${
|
|
options?.style === 'currency' ? ` ${options.currency}` : ''
|
|
}`;
|
|
};
|
|
|
|
/**
|
|
* Checks if the current entity state should be formatted as an integer based on the `state` and `step` attribute and returns the appropriate `Intl.NumberFormatOptions` object with `maximumFractionDigits` set
|
|
* @param entityState The state object of the entity
|
|
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
|
|
*/
|
|
export const getNumberFormatOptions = (
|
|
entityState: HassEntity,
|
|
entity?: EntityRegistryDisplayEntry,
|
|
): Intl.NumberFormatOptions | undefined => {
|
|
const precision = entity?.display_precision;
|
|
if (precision != null) {
|
|
return {
|
|
maximumFractionDigits: precision,
|
|
minimumFractionDigits: precision,
|
|
};
|
|
}
|
|
if (Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.state))) {
|
|
return { maximumFractionDigits: 0 };
|
|
}
|
|
if (entityState.attributes.step != null) {
|
|
return { maximumFractionDigits: Math.ceil(Math.log10(1 / entityState.attributes.step)) };
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* Generates default options for Intl.NumberFormat
|
|
* @param num The number to be formatted
|
|
* @param options The Intl.NumberFormatOptions that should be included in the returned options
|
|
*/
|
|
export const getDefaultFormatOptions = (
|
|
num: string | number,
|
|
options?: Intl.NumberFormatOptions,
|
|
): Intl.NumberFormatOptions => {
|
|
const defaultOptions: Intl.NumberFormatOptions = {
|
|
maximumFractionDigits: 2,
|
|
...options,
|
|
};
|
|
|
|
if (typeof num !== 'string') {
|
|
return defaultOptions;
|
|
}
|
|
|
|
// Keep decimal trailing zeros if they are present in a string numeric value
|
|
if (!options || (options.minimumFractionDigits === undefined && options.maximumFractionDigits === undefined)) {
|
|
const digits = num.indexOf('.') > -1 ? num.split('.')[1].length : 0;
|
|
defaultOptions.minimumFractionDigits = digits;
|
|
defaultOptions.maximumFractionDigits = digits;
|
|
}
|
|
|
|
return defaultOptions;
|
|
};
|