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 { 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
}`;
}

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 "../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,

View File

@ -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 "";

View File

@ -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>`;
}

View File

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

View File

@ -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>
`;
}

View File

@ -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>

View File

@ -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,

View File

@ -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 ||

View File

@ -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>

View File

@ -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>
`
: ""}