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 { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_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 { blankBeforePercent } from "../translations/blank_before_percent";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
@ -70,7 +74,11 @@ export const computeStateDisplayFromEntityAttributes = (
: attributes.unit_of_measurement === "%" : attributes.unit_of_measurement === "%"
? blankBeforePercent(locale) + "%" ? blankBeforePercent(locale) + "%"
: ` ${attributes.unit_of_measurement}`; : ` ${attributes.unit_of_measurement}`;
return `${formatNumber(state, locale)}${unit}`; return `${formatNumber(
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity)
)}${unit}`;
} }
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
@ -143,7 +151,12 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" || domain === "number" ||
domain === "input_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 // 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 { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round"; import { round } from "./round";
@ -9,9 +12,9 @@ import { round } from "./round";
export const isNumericState = (stateObj: HassEntity): boolean => export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = (attributes: { export const isNumericFromAttributes = (
[key: string]: any; attributes: HassEntityAttributeBase
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; ): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData 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 * Generates default options for Intl.NumberFormat
* @param num The number to be formatted * @param num The number to be formatted
* @param options The Intl.NumberFormatOptions that should be included in the returned options * @param options The Intl.NumberFormatOptions that should be included in the returned options
*/ */
const getDefaultFormatOptions = ( export const getDefaultFormatOptions = (
num: string | number, num: string | number,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => { ): Intl.NumberFormatOptions => {
@ -102,7 +122,8 @@ const getDefaultFormatOptions = (
// Keep decimal trailing zeros if they are present in a string numeric value // Keep decimal trailing zeros if they are present in a string numeric value
if ( if (
!options || !options ||
(!options.minimumFractionDigits && !options.maximumFractionDigits) (options.minimumFractionDigits === undefined &&
options.maximumFractionDigits === undefined)
) { ) {
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0; const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
defaultOptions.minimumFractionDigits = digits; 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 { computeStateName } from "../../common/entity/compute_state_name";
import { import {
formatNumber, formatNumber,
getNumberFormatOptions,
isNumericState, isNumericState,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
@ -149,7 +150,11 @@ export class HaStateLabelBadge extends LitElement {
entityState.state === UNAVAILABLE entityState.state === UNAVAILABLE
? "—" ? "—"
: isNumericState(entityState) : isNumericState(entityState)
? formatNumber(entityState.state, this.hass!.locale) ? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState)
)
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
entityState, entityState,

View File

@ -17,6 +17,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { import {
formatNumber, formatNumber,
getNumberFormatOptions,
isNumericState, isNumericState,
} from "../../../common/number/format_number"; } from "../../../common/number/format_number";
import { iconColorCSS } from "../../../common/style/icon_color_css"; 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") : this.hass.localize("state.default.unknown")
: isNumericState(stateObj) || this._config.unit : isNumericState(stateObj) || this._config.unit
? formatNumber(stateObj.state, this.hass.locale) ? formatNumber(
stateObj.state,
this.hass.locale,
getNumberFormatOptions(stateObj)
)
: computeStateDisplay( : computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,

View File

@ -1,6 +1,11 @@
import { assert } from "chai"; 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 { import {
FrontendLocaleData, FrontendLocaleData,
NumberFormat, NumberFormat,
@ -63,4 +68,80 @@ describe("formatNumber", () => {
"1,234.50" "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
);
});
}); });