mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 00:06:35 +00:00
Additional number formatting (#7763)
This commit is contained in:
parent
1d13947e71
commit
7403405d12
@ -5,7 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { numberFormat } from "../string/number-format";
|
||||
import { formatNumber } from "../string/format_number";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@ -20,7 +20,7 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${numberFormat(compareState, language)} ${
|
||||
return `${formatNumber(compareState, language)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
}
|
||||
|
54
src/common/string/format_number.ts
Normal file
54
src/common/string/format_number.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
language: string,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
// 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 (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(
|
||||
language,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = (
|
||||
num: string | number,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): Intl.NumberFormatOptions => {
|
||||
const defaultOptions: Intl.NumberFormatOptions = options || {};
|
||||
|
||||
if (typeof num !== "string") {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
// Keep decimal trailing zeros if they are present in a string numeric value
|
||||
if (
|
||||
!options ||
|
||||
(!options.minimumFractionDigits && !options.maximumFractionDigits)
|
||||
) {
|
||||
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
|
||||
defaultOptions.minimumFractionDigits = digits;
|
||||
defaultOptions.maximumFractionDigits = digits;
|
||||
}
|
||||
|
||||
return defaultOptions;
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const numberFormat = (
|
||||
num: string | number,
|
||||
language: string
|
||||
): string => {
|
||||
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(language).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
@ -21,6 +21,7 @@ import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { formatNumber } from "../../common/string/format_number";
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@ -115,7 +116,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: state.state === UNKNOWN
|
||||
? "-"
|
||||
: state.attributes.unit_of_measurement
|
||||
? state.state
|
||||
? formatNumber(state.state, this.hass!.language)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
state,
|
||||
|
@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
class HaClimateState extends LitElement {
|
||||
@ -51,11 +52,17 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_temperature,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${this.stateObj.attributes.current_humidity} %`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass!.language
|
||||
)} %`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@ -70,21 +77,39 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_temp_low,
|
||||
this.hass!.language
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_temp_high,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_low,
|
||||
this.hass!.language
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_high,
|
||||
this.hass!.language
|
||||
)}%`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${this.stateObj.attributes.humidity} %`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass!.language
|
||||
)} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
@ -12,6 +12,7 @@ import { afterNextRender } from "../common/util/render-status";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
@ -29,6 +30,8 @@ export class Gauge extends LitElement {
|
||||
|
||||
@property({ type: Number }) public value = 0;
|
||||
|
||||
@property({ type: String }) public language = "";
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@internalProperty() private _angle = 0;
|
||||
@ -88,7 +91,7 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${this.value} ${this.label}
|
||||
${formatNumber(this.value, this.language)} ${this.label}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
import "../components/ha-icon";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HomeAssistant, WeatherEntity } from "../types";
|
||||
import { roundWithOneDecimal } from "../util/calculate";
|
||||
|
||||
export const weatherSVGs = new Set<string>([
|
||||
"clear-night",
|
||||
@ -106,15 +106,19 @@ export const getWind = (
|
||||
speed: string,
|
||||
bearing: string
|
||||
): string => {
|
||||
const speedText = `${formatNumber(speed, hass!.language)} ${getWeatherUnit(
|
||||
hass!,
|
||||
"wind_speed"
|
||||
)}`;
|
||||
if (bearing !== null) {
|
||||
const cardinalDirection = getWindBearing(bearing);
|
||||
return `${speed} ${getWeatherUnit(hass!, "wind_speed")} (${
|
||||
return `${speedText} (${
|
||||
hass.localize(
|
||||
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
|
||||
) || cardinalDirection
|
||||
})`;
|
||||
}
|
||||
return `${speed} ${getWeatherUnit(hass!, "wind_speed")}`;
|
||||
return speedText;
|
||||
};
|
||||
|
||||
export const getWeatherUnit = (
|
||||
@ -175,7 +179,8 @@ export const getSecondaryWeatherAttribute = (
|
||||
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
|
||||
`
|
||||
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
|
||||
${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
|
||||
${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })}
|
||||
${getWeatherUnit(hass!, attribute)}
|
||||
`;
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { formatTime } from "../../../common/datetime/format_time";
|
||||
import { formatNumber } from "../../../common/string/format_number";
|
||||
import "../../../components/ha-relative-time";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@ -59,7 +60,12 @@ class MoreInfoSun extends LitElement {
|
||||
<div class="key">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
|
||||
</div>
|
||||
<div class="value">${this.stateObj.attributes.elevation}</div>
|
||||
<div class="value">
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.elevation,
|
||||
this.hass!.language
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
mdiWeatherWindy,
|
||||
mdiWeatherWindyVariant,
|
||||
} from "@mdi/js";
|
||||
import { formatNumber } from "../../../common/string/format_number";
|
||||
|
||||
const weatherIcons = {
|
||||
"clear-night": mdiWeatherNight,
|
||||
@ -88,7 +89,10 @@ class MoreInfoWeather extends LitElement {
|
||||
${this.hass.localize("ui.card.weather.attributes.temperature")}
|
||||
</div>
|
||||
<div>
|
||||
${this.stateObj.attributes.temperature}
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass!.language
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "temperature")}
|
||||
</div>
|
||||
</div>
|
||||
@ -100,7 +104,10 @@ class MoreInfoWeather extends LitElement {
|
||||
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
|
||||
</div>
|
||||
<div>
|
||||
${this.stateObj.attributes.pressure}
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.pressure,
|
||||
this.hass!.language
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "air_pressure")}
|
||||
</div>
|
||||
</div>
|
||||
@ -113,7 +120,13 @@ class MoreInfoWeather extends LitElement {
|
||||
<div class="main">
|
||||
${this.hass.localize("ui.card.weather.attributes.humidity")}
|
||||
</div>
|
||||
<div>${this.stateObj.attributes.humidity} %</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass!.language
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@ -142,7 +155,10 @@ class MoreInfoWeather extends LitElement {
|
||||
${this.hass.localize("ui.card.weather.attributes.visibility")}
|
||||
</div>
|
||||
<div>
|
||||
${this.stateObj.attributes.visibility}
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.visibility,
|
||||
this.hass!.language
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "length")}
|
||||
</div>
|
||||
</div>
|
||||
@ -176,13 +192,13 @@ class MoreInfoWeather extends LitElement {
|
||||
${this.computeDate(item.datetime)}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${item.templow}
|
||||
${formatNumber(item.templow, this.hass!.language)}
|
||||
${getWeatherUnit(this.hass, "temperature")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="temp">
|
||||
${item.temperature}
|
||||
${formatNumber(item.temperature, this.hass!.language)}
|
||||
${getWeatherUnit(this.hass, "temperature")}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import { EntityCardConfig } from "./types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { formatNumber } from "../../../common/string/format_number";
|
||||
|
||||
@customElement("hui-entity-card")
|
||||
export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
@ -128,7 +129,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
? stateObj.attributes[this._config.attribute!] ??
|
||||
this.hass.localize("state.default.unknown")
|
||||
: stateObj.attributes.unit_of_measurement
|
||||
? stateObj.state
|
||||
? formatNumber(stateObj.state, this.hass!.language)
|
||||
: computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
|
@ -128,6 +128,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
.min=${this._config.min!}
|
||||
.max=${this._config.max!}
|
||||
.value=${state}
|
||||
.language=${this.hass!.language}
|
||||
.label=${this._config!.unit ||
|
||||
this.hass?.states[this._config!.entity].attributes
|
||||
.unit_of_measurement ||
|
||||
|
@ -19,6 +19,7 @@ import { UNIT_F } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../../common/string/format_number";
|
||||
import "../../../components/ha-card";
|
||||
import type { HaCard } from "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@ -141,7 +142,10 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
text-anchor="middle"
|
||||
style="font-size: 13px;"
|
||||
>
|
||||
${stateObj.attributes.current_temperature}
|
||||
${formatNumber(
|
||||
stateObj.attributes.current_temperature,
|
||||
this.hass!.language
|
||||
)}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</tspan>
|
||||
@ -162,19 +166,34 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
: Array.isArray(this._setTemp)
|
||||
? this._stepSize === 1
|
||||
? svg`
|
||||
${this._setTemp[0].toFixed()} -
|
||||
${this._setTemp[1].toFixed()}
|
||||
${formatNumber(this._setTemp[0], this.hass!.language, {
|
||||
maximumFractionDigits: 0,
|
||||
})} -
|
||||
${formatNumber(this._setTemp[1], this.hass!.language, {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
`
|
||||
: svg`
|
||||
${this._setTemp[0].toFixed(1)} -
|
||||
${this._setTemp[1].toFixed(1)}
|
||||
${formatNumber(this._setTemp[0], this.hass!.language, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})} -
|
||||
${formatNumber(this._setTemp[1], this.hass!.language, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
`
|
||||
: this._stepSize === 1
|
||||
? svg`
|
||||
${this._setTemp.toFixed()}
|
||||
${formatNumber(this._setTemp, this.hass!.language, {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
`
|
||||
: svg`
|
||||
${this._setTemp.toFixed(1)}
|
||||
${formatNumber(this._setTemp, this.hass!.language, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
`
|
||||
}
|
||||
</text>
|
||||
|
@ -15,6 +15,7 @@ import { computeStateDisplay } from "../../../common/entity/compute_state_displa
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateIcon } from "../../../common/entity/state_icon";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { formatNumber } from "../../../common/string/format_number";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
@ -214,9 +215,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
<div class="temp-attribute">
|
||||
<div class="temp">
|
||||
${stateObj.attributes.temperature}<span
|
||||
>${getWeatherUnit(this.hass, "temperature")}</span
|
||||
>
|
||||
${formatNumber(
|
||||
stateObj.attributes.temperature,
|
||||
this.hass!.language
|
||||
)}<span>${getWeatherUnit(this.hass, "temperature")}</span>
|
||||
</div>
|
||||
<div class="attribute">
|
||||
${this._config.secondary_info_attribute !== undefined
|
||||
@ -241,9 +243,12 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
stateObj.attributes.wind_bearing
|
||||
)
|
||||
: html`
|
||||
${stateObj.attributes[
|
||||
this._config.secondary_info_attribute
|
||||
]}
|
||||
${formatNumber(
|
||||
stateObj.attributes[
|
||||
this._config.secondary_info_attribute
|
||||
],
|
||||
this.hass!.language
|
||||
)}
|
||||
${getWeatherUnit(
|
||||
this.hass,
|
||||
this._config.secondary_info_attribute
|
||||
@ -307,14 +312,20 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
item.temperature !== null
|
||||
? html`
|
||||
<div class="temp">
|
||||
${item.temperature}°
|
||||
${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.language
|
||||
)}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.templow !== undefined && item.templow !== null
|
||||
? html`
|
||||
<div class="templow">
|
||||
${item.templow}°
|
||||
${formatNumber(
|
||||
item.templow,
|
||||
this.hass!.language
|
||||
)}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
Loading…
x
Reference in New Issue
Block a user