Additional number formatting (#7763)

This commit is contained in:
Josh McCarty 2020-11-25 03:37:58 -07:00 committed by GitHub
parent 1d13947e71
commit 7403405d12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 179 additions and 59 deletions

View File

@ -5,7 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { numberFormat } from "../string/number-format"; import { formatNumber } from "../string/format_number";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
@ -20,7 +20,7 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
return `${numberFormat(compareState, language)} ${ return `${formatNumber(compareState, language)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
} }

View 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;
};

View File

@ -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();
};

View File

@ -21,6 +21,7 @@ import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { formatNumber } from "../../common/string/format_number";
@customElement("ha-state-label-badge") @customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement { export class HaStateLabelBadge extends LitElement {
@ -115,7 +116,7 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN : state.state === UNKNOWN
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? state.state ? formatNumber(state.state, this.hass!.language)
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
state, state,

View File

@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { formatNumber } from "../common/string/format_number";
@customElement("ha-climate-state") @customElement("ha-climate-state")
class HaClimateState extends LitElement { class HaClimateState extends LitElement {
@ -51,11 +52,17 @@ class HaClimateState extends LitElement {
} }
if (this.stateObj.attributes.current_temperature != null) { 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) { 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; return undefined;
@ -70,21 +77,39 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.target_temp_low != null && this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != 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) { 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 ( if (
this.stateObj.attributes.target_humidity_low != null && this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != 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) { if (this.stateObj.attributes.humidity != null) {
return `${this.stateObj.attributes.humidity} %`; return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass!.language
)} %`;
} }
return ""; return "";

View File

@ -12,6 +12,7 @@ import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
import { formatNumber } from "../common/string/format_number";
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); 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: Number }) public value = 0;
@property({ type: String }) public language = "";
@property() public label = ""; @property() public label = "";
@internalProperty() private _angle = 0; @internalProperty() private _angle = 0;
@ -88,7 +91,7 @@ export class Gauge extends LitElement {
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
${this.value} ${this.label} ${formatNumber(this.value, this.language)} ${this.label}
</text> </text>
</svg>`; </svg>`;
} }

View File

@ -7,10 +7,10 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element"; import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { formatNumber } from "../common/string/format_number";
import "../components/ha-icon"; import "../components/ha-icon";
import "../components/ha-svg-icon"; import "../components/ha-svg-icon";
import type { HomeAssistant, WeatherEntity } from "../types"; import type { HomeAssistant, WeatherEntity } from "../types";
import { roundWithOneDecimal } from "../util/calculate";
export const weatherSVGs = new Set<string>([ export const weatherSVGs = new Set<string>([
"clear-night", "clear-night",
@ -106,15 +106,19 @@ export const getWind = (
speed: string, speed: string,
bearing: string bearing: string
): string => { ): string => {
const speedText = `${formatNumber(speed, hass!.language)} ${getWeatherUnit(
hass!,
"wind_speed"
)}`;
if (bearing !== null) { if (bearing !== null) {
const cardinalDirection = getWindBearing(bearing); const cardinalDirection = getWindBearing(bearing);
return `${speed} ${getWeatherUnit(hass!, "wind_speed")} (${ return `${speedText} (${
hass.localize( hass.localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}` `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection ) || cardinalDirection
})`; })`;
} }
return `${speed} ${getWeatherUnit(hass!, "wind_speed")}`; return speedText;
}; };
export const getWeatherUnit = ( export const getWeatherUnit = (
@ -175,7 +179,8 @@ export const getSecondaryWeatherAttribute = (
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon> <ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
` `
: hass!.localize(`ui.card.weather.attributes.${attribute}`)} : hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)} ${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, attribute)}
`; `;
}; };

View File

@ -9,6 +9,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { formatTime } from "../../../common/datetime/format_time"; import { formatTime } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/string/format_number";
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -59,7 +60,12 @@ class MoreInfoSun extends LitElement {
<div class="key"> <div class="key">
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")} ${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div> </div>
<div class="value">${this.stateObj.attributes.elevation}</div> <div class="value">
${formatNumber(
this.stateObj.attributes.elevation,
this.hass!.language
)}
</div>
</div> </div>
`; `;
} }

View File

@ -34,6 +34,7 @@ import {
mdiWeatherWindy, mdiWeatherWindy,
mdiWeatherWindyVariant, mdiWeatherWindyVariant,
} from "@mdi/js"; } from "@mdi/js";
import { formatNumber } from "../../../common/string/format_number";
const weatherIcons = { const weatherIcons = {
"clear-night": mdiWeatherNight, "clear-night": mdiWeatherNight,
@ -88,7 +89,10 @@ class MoreInfoWeather extends LitElement {
${this.hass.localize("ui.card.weather.attributes.temperature")} ${this.hass.localize("ui.card.weather.attributes.temperature")}
</div> </div>
<div> <div>
${this.stateObj.attributes.temperature} ${formatNumber(
this.stateObj.attributes.temperature,
this.hass!.language
)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
</div> </div>
@ -100,7 +104,10 @@ class MoreInfoWeather extends LitElement {
${this.hass.localize("ui.card.weather.attributes.air_pressure")} ${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div> </div>
<div> <div>
${this.stateObj.attributes.pressure} ${formatNumber(
this.stateObj.attributes.pressure,
this.hass!.language
)}
${getWeatherUnit(this.hass, "air_pressure")} ${getWeatherUnit(this.hass, "air_pressure")}
</div> </div>
</div> </div>
@ -113,7 +120,13 @@ class MoreInfoWeather extends LitElement {
<div class="main"> <div class="main">
${this.hass.localize("ui.card.weather.attributes.humidity")} ${this.hass.localize("ui.card.weather.attributes.humidity")}
</div> </div>
<div>${this.stateObj.attributes.humidity} %</div> <div>
${formatNumber(
this.stateObj.attributes.humidity,
this.hass!.language
)}
%
</div>
</div> </div>
` `
: ""} : ""}
@ -142,7 +155,10 @@ class MoreInfoWeather extends LitElement {
${this.hass.localize("ui.card.weather.attributes.visibility")} ${this.hass.localize("ui.card.weather.attributes.visibility")}
</div> </div>
<div> <div>
${this.stateObj.attributes.visibility} ${formatNumber(
this.stateObj.attributes.visibility,
this.hass!.language
)}
${getWeatherUnit(this.hass, "length")} ${getWeatherUnit(this.hass, "length")}
</div> </div>
</div> </div>
@ -176,13 +192,13 @@ class MoreInfoWeather extends LitElement {
${this.computeDate(item.datetime)} ${this.computeDate(item.datetime)}
</div> </div>
<div class="templow"> <div class="templow">
${item.templow} ${formatNumber(item.templow, this.hass!.language)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
` `
: ""} : ""}
<div class="temp"> <div class="temp">
${item.temperature} ${formatNumber(item.temperature, this.hass!.language)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
</div> </div>

View File

@ -31,6 +31,7 @@ import {
import { HuiErrorCard } from "./hui-error-card"; import { HuiErrorCard } from "./hui-error-card";
import { EntityCardConfig } from "./types"; import { EntityCardConfig } from "./types";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
import { formatNumber } from "../../../common/string/format_number";
@customElement("hui-entity-card") @customElement("hui-entity-card")
export class HuiEntityCard extends LitElement implements LovelaceCard { export class HuiEntityCard extends LitElement implements LovelaceCard {
@ -128,7 +129,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? stateObj.attributes[this._config.attribute!] ?? ? stateObj.attributes[this._config.attribute!] ??
this.hass.localize("state.default.unknown") this.hass.localize("state.default.unknown")
: stateObj.attributes.unit_of_measurement : stateObj.attributes.unit_of_measurement
? stateObj.state ? formatNumber(stateObj.state, this.hass!.language)
: computeStateDisplay( : computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,

View File

@ -128,6 +128,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
.min=${this._config.min!} .min=${this._config.min!}
.max=${this._config.max!} .max=${this._config.max!}
.value=${state} .value=${state}
.language=${this.hass!.language}
.label=${this._config!.unit || .label=${this._config!.unit ||
this.hass?.states[this._config!.entity].attributes this.hass?.states[this._config!.entity].attributes
.unit_of_measurement || .unit_of_measurement ||

View File

@ -19,6 +19,7 @@ import { UNIT_F } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { formatNumber } from "../../../common/string/format_number";
import "../../../components/ha-card"; import "../../../components/ha-card";
import type { HaCard } from "../../../components/ha-card"; import type { HaCard } from "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
@ -141,7 +142,10 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
text-anchor="middle" text-anchor="middle"
style="font-size: 13px;" 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;"> <tspan dx="-3" dy="-6.5" style="font-size: 4px;">
${this.hass.config.unit_system.temperature} ${this.hass.config.unit_system.temperature}
</tspan> </tspan>
@ -162,19 +166,34 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
: Array.isArray(this._setTemp) : Array.isArray(this._setTemp)
? this._stepSize === 1 ? this._stepSize === 1
? svg` ? svg`
${this._setTemp[0].toFixed()} - ${formatNumber(this._setTemp[0], this.hass!.language, {
${this._setTemp[1].toFixed()} maximumFractionDigits: 0,
})} -
${formatNumber(this._setTemp[1], this.hass!.language, {
maximumFractionDigits: 0,
})}
` `
: svg` : svg`
${this._setTemp[0].toFixed(1)} - ${formatNumber(this._setTemp[0], this.hass!.language, {
${this._setTemp[1].toFixed(1)} minimumFractionDigits: 1,
maximumFractionDigits: 1,
})} -
${formatNumber(this._setTemp[1], this.hass!.language, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
` `
: this._stepSize === 1 : this._stepSize === 1
? svg` ? svg`
${this._setTemp.toFixed()} ${formatNumber(this._setTemp, this.hass!.language, {
maximumFractionDigits: 0,
})}
` `
: svg` : svg`
${this._setTemp.toFixed(1)} ${formatNumber(this._setTemp, this.hass!.language, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
` `
} }
</text> </text>

View File

@ -15,6 +15,7 @@ import { computeStateDisplay } from "../../../common/entity/compute_state_displa
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon"; import { stateIcon } from "../../../common/entity/state_icon";
import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { formatNumber } from "../../../common/string/format_number";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
@ -214,9 +215,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="temp-attribute"> <div class="temp-attribute">
<div class="temp"> <div class="temp">
${stateObj.attributes.temperature}<span ${formatNumber(
>${getWeatherUnit(this.hass, "temperature")}</span stateObj.attributes.temperature,
> this.hass!.language
)}<span>${getWeatherUnit(this.hass, "temperature")}</span>
</div> </div>
<div class="attribute"> <div class="attribute">
${this._config.secondary_info_attribute !== undefined ${this._config.secondary_info_attribute !== undefined
@ -241,9 +243,12 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
stateObj.attributes.wind_bearing stateObj.attributes.wind_bearing
) )
: html` : html`
${stateObj.attributes[ ${formatNumber(
this._config.secondary_info_attribute stateObj.attributes[
]} this._config.secondary_info_attribute
],
this.hass!.language
)}
${getWeatherUnit( ${getWeatherUnit(
this.hass, this.hass,
this._config.secondary_info_attribute this._config.secondary_info_attribute
@ -307,14 +312,20 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
item.temperature !== null item.temperature !== null
? html` ? html`
<div class="temp"> <div class="temp">
${item.temperature}° ${formatNumber(
item.temperature,
this.hass!.language
)}°
</div> </div>
` `
: ""} : ""}
${item.templow !== undefined && item.templow !== null ${item.templow !== undefined && item.templow !== null
? html` ? html`
<div class="templow"> <div class="templow">
${item.templow}° ${formatNumber(
item.templow,
this.hass!.language
)}°
</div> </div>
` `
: ""} : ""}