Format numeric entities with integer value and step as integer instead of float (#14112)

This commit is contained in:
Josh McCarty 2022-10-26 04:27:14 -07:00 committed by GitHub
parent d445bf2505
commit 66ed1b18be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 12 deletions

View File

@ -9,7 +9,11 @@ import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericFromAttributes } from "../number/format_number";
import {
formatNumber,
getNumberFormatOptions,
isNumericFromAttributes,
} from "../number/format_number";
import { blankBeforePercent } from "../translations/blank_before_percent";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
@ -70,7 +74,11 @@ export const computeStateDisplayFromEntityAttributes = (
: attributes.unit_of_measurement === "%"
? blankBeforePercent(locale) + "%"
: ` ${attributes.unit_of_measurement}`;
return `${formatNumber(state, locale)}${unit}`;
return `${formatNumber(
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity)
)}${unit}`;
}
const domain = computeDomain(entityId);
@ -143,7 +151,12 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" ||
domain === "input_number"
) {
return formatNumber(state, locale);
// Format as an integer if the value and step are integers
return formatNumber(
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity)
);
}
// state of button is a timestamp

View File

@ -1,4 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round";
@ -9,9 +12,9 @@ import { round } from "./round";
export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = (attributes: {
[key: string]: any;
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const isNumericFromAttributes = (
attributes: HassEntityAttributeBase
): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData
@ -81,12 +84,29 @@ export const formatNumber = (
}`;
};
/**
* 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
): Intl.NumberFormatOptions | undefined => {
if (
Number.isInteger(Number(entityState.attributes?.step)) &&
Number.isInteger(Number(entityState.state))
) {
return { maximumFractionDigits: 0 };
}
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
*/
const getDefaultFormatOptions = (
export const getDefaultFormatOptions = (
num: string | number,
options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => {
@ -102,7 +122,8 @@ const getDefaultFormatOptions = (
// Keep decimal trailing zeros if they are present in a string numeric value
if (
!options ||
(!options.minimumFractionDigits && !options.maximumFractionDigits)
(options.minimumFractionDigits === undefined &&
options.maximumFractionDigits === undefined)
) {
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
defaultOptions.minimumFractionDigits = digits;

View File

@ -16,6 +16,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
@ -149,7 +150,11 @@ export class HaStateLabelBadge extends LitElement {
entityState.state === UNAVAILABLE
? "—"
: isNumericState(entityState)
? formatNumber(entityState.state, this.hass!.locale)
? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState)
)
: computeStateDisplay(
this.hass!.localize,
entityState,

View File

@ -17,6 +17,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../../common/number/format_number";
import { iconColorCSS } from "../../../common/style/icon_color_css";
@ -147,7 +148,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
)
: this.hass.localize("state.default.unknown")
: isNumericState(stateObj) || this._config.unit
? formatNumber(stateObj.state, this.hass.locale)
? formatNumber(
stateObj.state,
this.hass.locale,
getNumberFormatOptions(stateObj)
)
: computeStateDisplay(
this.hass.localize,
stateObj,

View File

@ -1,6 +1,11 @@
import { assert } from "chai";
import { HassEntity } from "home-assistant-js-websocket";
import { formatNumber } from "../../../src/common/number/format_number";
import {
formatNumber,
getDefaultFormatOptions,
getNumberFormatOptions,
} from "../../../src/common/number/format_number";
import {
FrontendLocaleData,
NumberFormat,
@ -63,4 +68,80 @@ describe("formatNumber", () => {
"1,234.50"
);
});
it("Sets only the maximumFractionDigits format option when none are provided for a number value", () => {
assert.deepEqual(getDefaultFormatOptions(1234.5), {
maximumFractionDigits: 2,
});
});
it("Sets minimumFractionDigits and maximumFractionDigits to '2' when none are provided for a string numeric value with two decimal places", () => {
assert.deepEqual(getDefaultFormatOptions("1234.50"), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
});
it("Merges default format options (minimumFractionDigits and maximumFractionDigits) and non-default format options for a string numeric value with two decimal places", () => {
assert.deepEqual(getDefaultFormatOptions("1234.50", { currency: "USD" }), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: "USD",
});
});
it("Sets maximumFractionDigits when that is the only format option provided", () => {
assert.deepEqual(
getDefaultFormatOptions("1234.50", { maximumFractionDigits: 0 }),
{
maximumFractionDigits: 0,
}
);
});
it("Sets maximumFractionDigits to '2' and minimumFractionDigits to the provided value when only minimumFractionDigits is provided", () => {
assert.deepEqual(
getDefaultFormatOptions("1234.50", { minimumFractionDigits: 1 }),
{
minimumFractionDigits: 1,
maximumFractionDigits: 2,
}
);
});
it("Sets maximumFractionDigits to '0' when the state value and step are integers", () => {
assert.deepEqual(
getNumberFormatOptions({
state: "3.0",
attributes: { step: 1 },
} as unknown as HassEntity),
{
maximumFractionDigits: 0,
}
);
});
it("Does not set any Intl.NumberFormatOptions when the step is not an integer", () => {
assert.strictEqual(
getNumberFormatOptions({
state: "3.0",
attributes: { step: 0.5 },
} as unknown as HassEntity),
undefined
);
});
it("Does not set any Intl.NumberFormatOptions when the state value is not an integer", () => {
assert.strictEqual(
getNumberFormatOptions({ state: "3.5" } as unknown as HassEntity),
undefined
);
});
it("Does not set any Intl.NumberFormatOptions when there is no step attribute", () => {
assert.strictEqual(
getNumberFormatOptions({ state: "3.0" } as unknown as HassEntity),
undefined
);
});
});