Skip to content
Snippets Groups Projects
Unverified Commit f5d85e81 authored by Gustavo Saiani's avatar Gustavo Saiani Committed by GitHub
Browse files

Break down formatting lib and add TypeScript (#24544)

parent f6fb59a4
No related branches found
No related tags found
No related merge requests found
Showing
with 872 additions and 1002 deletions
......@@ -5,6 +5,8 @@ export interface DatasetColumn {
display_name: string;
source: string;
name: string;
remapped_to_column?: DatasetColumn;
unit?: string;
}
export interface DatasetData {
......
export interface Field {
id: number;
dimensions?: FieldDimension;
display_name: string;
table_id: number;
name: string;
base_type: string;
description: string | null;
nfc_path: string[] | null;
}
export type FieldDimension = {
name: string;
};
......@@ -22,3 +22,21 @@ export interface NativeDatasetQuery {
}
export type DatasetQuery = StructuredDatasetQuery | NativeDatasetQuery;
export type DatetimeUnit =
| "default"
| "minute"
| "minute-of-hour"
| "hour"
| "hour-of-day"
| "day"
| "day-of-week"
| "day-of-month"
| "day-of-year"
| "week"
| "week-of-year"
| "month"
| "month-of-year"
| "quarter"
| "quarter-of-year"
| "year";
import { ISO8601Time } from ".";
import { FieldId } from "./Field";
import { DatasetQuery } from "./Card";
import { DatetimeUnit, FieldLiteral, Field } from "./Query";
import { FieldLiteral, Field } from "./Query";
import { DatetimeUnit } from "metabase-types/api/query";
export type ColumnName = string;
......
......@@ -3,6 +3,7 @@ import { FieldId, BaseType } from "./Field";
import { SegmentId } from "./Segment";
import { MetricId } from "./Metric";
import { ParameterType } from "./Parameter";
import { DatetimeUnit } from "metabase-types/api/query";
export type ExpressionName = string;
......@@ -27,23 +28,6 @@ export type RelativeDatetimeUnit =
| "month"
| "quarter"
| "year";
export type DatetimeUnit =
| "default"
| "minute"
| "minute-of-hour"
| "hour"
| "hour-of-day"
| "day"
| "day-of-week"
| "day-of-month"
| "day-of-year"
| "week"
| "week-of-year"
| "month"
| "month-of-year"
| "quarter"
| "quarter-of-year"
| "year";
export type TemplateTagId = string;
export type TemplateTagName = string;
......
This diff is collapsed.
import { color } from "metabase/lib/colors";
export function assignUserColors(
userIds: string[],
currentUserId: string,
colors = [
color("brand"),
color("accent2"),
color("error"),
color("accent1"),
color("accent4"),
color("bg-medium"),
],
) {
const assignments: { [index: string]: string } = {};
const currentUserColor = colors[0];
const otherUserColors = colors.slice(1);
let otherUserColorIndex = 0;
for (const userId of userIds) {
if (!(userId in assignments)) {
if (userId === currentUserId) {
assignments[userId] = currentUserColor;
} else if (userId != null) {
assignments[userId] =
otherUserColors[otherUserColorIndex++ % otherUserColors.length];
}
}
}
return assignments;
}
import { capitalize } from "./strings";
import { getFriendlyName } from "metabase/visualizations/lib/utils";
import type { DatasetColumn } from "metabase-types/api/dataset";
export function formatColumn(column: DatasetColumn): string {
if (!column) {
return "";
} else if (column.remapped_to_column != null) {
// remapped_to_column is a special field added by Visualization.jsx
return formatColumn(column.remapped_to_column);
} else {
let columnTitle = getFriendlyName(column);
if (column.unit && column.unit !== "default") {
columnTitle += ": " + capitalize(column.unit.replace(/-/g, " "));
}
return columnTitle;
}
}
import { currency } from "cljs/metabase.shared.util.currency";
let currencyMapCache;
export function getCurrencySymbol(currencyCode) {
if (!currencyMapCache) {
// only turn the array into a map if we call this function
currencyMapCache = Object.fromEntries(currency);
}
return currencyMapCache[currencyCode]?.symbol || currencyCode || "$";
}
export const COMPACT_CURRENCY_OPTIONS = {
// Currencies vary in how many decimals they display, so this is probably
// wrong in some cases. Intl.NumberFormat has some of that data built-in, but
// I couldn't figure out how to use it here.
digits: 2,
currency_style: "symbol",
};
import React from "react";
import moment from "moment-timezone";
import { parseTimestamp } from "metabase/lib/time";
import { isDateWithoutTime } from "metabase/lib/schema_metadata";
import {
DEFAULT_DATE_STYLE,
DEFAULT_TIME_STYLE,
getTimeFormatFromStyle,
hasHour,
} from "./datetime-utils";
const RANGE_SEPARATOR = ` – `;
const DEFAULT_DATE_FORMATS = {
year: "YYYY",
......@@ -36,7 +48,11 @@ const DATE_STYLE_TO_FORMAT = {
},
};
export const DEFAULT_DATE_STYLE = "MMMM D, YYYY";
const getDayFormat = options =>
options.compact || options.date_abbreviate ? "ddd" : "dddd";
const getMonthFormat = options =>
options.compact || options.date_abbreviate ? "MMM" : "MMMM";
export function getDateFormatFromStyle(style, unit, separator) {
const replaceSeparators = format =>
......@@ -58,28 +74,6 @@ export function getDateFormatFromStyle(style, unit, separator) {
return replaceSeparators(style);
}
const UNITS_WITH_HOUR = ["default", "minute", "hour", "hour-of-day"];
const UNITS_WITH_DAY = ["default", "minute", "hour", "day", "week"];
const UNITS_WITH_HOUR_SET = new Set(UNITS_WITH_HOUR);
const UNITS_WITH_DAY_SET = new Set(UNITS_WITH_DAY);
export const hasHour = unit => unit == null || UNITS_WITH_HOUR_SET.has(unit);
export const hasDay = unit => unit == null || UNITS_WITH_DAY_SET.has(unit);
export const DEFAULT_TIME_STYLE = "h:mm A";
export function getTimeFormatFromStyle(style, unit, timeEnabled) {
const format = style;
if (!timeEnabled || timeEnabled === "milliseconds") {
return format.replace(/mm/, "mm:ss.SSS");
} else if (timeEnabled === "seconds") {
return format.replace(/mm/, "mm:ss");
} else {
return format;
}
}
export function formatDateTimeForParameter(value, unit) {
const m = parseTimestamp(value, unit);
if (!m.isValid()) {
......@@ -107,3 +101,177 @@ export function formatDateTimeForParameter(value, unit) {
: `${start.format("YYYY-MM-DD")}~${end.format("YYYY-MM-DD")}`;
}
}
/** This formats a time with unit as a date range */
export function formatDateTimeRangeWithUnit(value, unit, options = {}) {
const m = parseTimestamp(value, unit, options.local);
if (!m.isValid()) {
return String(value);
}
// Tooltips should show full month name, but condense "MMMM D, YYYY - MMMM D, YYYY" to "MMMM D - D, YYYY" etc
const monthFormat =
options.type === "tooltip" ? "MMMM" : getMonthFormat(options);
const condensed = options.compact || options.type === "tooltip";
// The client's unit boundaries might not line up with the data returned from the server.
// We shift the range so that the start lines up with the value.
const start = m.clone().startOf(unit);
const end = m.clone().endOf(unit);
const shift = m.diff(start, "days");
[start, end].forEach(d => d.add(shift, "days"));
if (start.isValid() && end.isValid()) {
if (!condensed || start.year() !== end.year()) {
// January 1, 2018 - January 2, 2019
return (
start.format(`${monthFormat} D, YYYY`) +
RANGE_SEPARATOR +
end.format(`${monthFormat} D, YYYY`)
);
} else if (start.month() !== end.month()) {
// January 1 - Feburary 2, 2018
return (
start.format(`${monthFormat} D`) +
RANGE_SEPARATOR +
end.format(`${monthFormat} D, YYYY`)
);
} else {
// January 1 - 2, 2018
return (
start.format(`${monthFormat} D`) +
RANGE_SEPARATOR +
end.format(`D, YYYY`)
);
}
} else {
// TODO: when is this used?
return formatWeek(m, options);
}
}
export function formatRange(range, formatter, options = {}) {
const [start, end] = range.map(value => formatter(value, options));
if ((options.jsx && typeof start !== "string") || typeof end !== "string") {
return (
<span>
{start} {RANGE_SEPARATOR} {end}
</span>
);
} else {
return `${start} ${RANGE_SEPARATOR} ${end}`;
}
}
function formatWeek(m, options = {}) {
return formatMajorMinor(m.format("wo"), m.format("gggg"), options);
}
function formatMajorMinor(major, minor, options = {}) {
options = {
jsx: false,
majorWidth: 3,
...options,
};
if (options.jsx) {
return (
<span>
<span
style={{ minWidth: options.majorWidth + "em" }}
className="inline-block text-right text-bold"
>
{major}
</span>
{" - "}
<span>{minor}</span>
</span>
);
} else {
return `${major} - ${minor}`;
}
}
function replaceDateFormatNames(format, options) {
return format
.replace(/\bMMMM\b/g, getMonthFormat(options))
.replace(/\bdddd\b/g, getDayFormat(options));
}
function formatDateTimeWithFormats(value, dateFormat, timeFormat, options) {
const m = parseTimestamp(
value,
options.column && options.column.unit,
options.local,
);
if (!m.isValid()) {
return String(value);
}
const format = [];
if (dateFormat) {
format.push(replaceDateFormatNames(dateFormat, options));
}
const shouldIncludeTime =
timeFormat && options.time_enabled && !isDateWithoutTime(options.column);
if (shouldIncludeTime) {
format.push(timeFormat);
}
return m.format(format.join(", "));
}
export function formatDateTimeWithUnit(value, unit, options = {}) {
if (options.isExclude && unit === "hour-of-day") {
return moment.utc(value).format("h A");
} else if (options.isExclude && unit === "day-of-week") {
const date = moment.utc(value);
if (date.isValid()) {
return date.format("dddd");
}
}
const m = parseTimestamp(value, unit, options.local);
if (!m.isValid()) {
return String(value);
}
// expand "week" into a range in specific contexts
if (unit === "week") {
if (
(options.type === "tooltip" || options.type === "cell") &&
!options.noRange
) {
// tooltip show range like "January 1 - 7, 2017"
return formatDateTimeRangeWithUnit(value, unit, options);
}
}
options = {
date_style: DEFAULT_DATE_STYLE,
time_style: DEFAULT_TIME_STYLE,
time_enabled: hasHour(unit) ? "minutes" : null,
...options,
};
let dateFormat = options.date_format;
let timeFormat = options.time_format;
if (!dateFormat) {
dateFormat = getDateFormatFromStyle(
options["date_style"],
unit,
options["date_separator"],
);
}
if (!timeFormat) {
timeFormat = getTimeFormatFromStyle(
options.time_style,
unit,
options.time_enabled,
);
}
return formatDateTimeWithFormats(m, dateFormat, timeFormat, options);
}
import type { DatetimeUnit } from "metabase-types/api/query";
export const DEFAULT_TIME_STYLE = "h:mm A";
export const DEFAULT_DATE_STYLE = "MMMM D, YYYY";
const UNITS_WITH_HOUR = ["default", "minute", "hour", "hour-of-day"] as const;
const UNITS_WITH_DAY = ["default", "minute", "hour", "day", "week"] as const;
type UNITS_WITH_HOUR_TYPE = typeof UNITS_WITH_HOUR[number];
type UNITS_WITH_DAY_TYPE = typeof UNITS_WITH_DAY[number];
const UNITS_WITH_HOUR_SET = new Set(UNITS_WITH_HOUR);
const UNITS_WITH_DAY_SET = new Set(UNITS_WITH_DAY);
export const hasDay = (unit: DatetimeUnit) =>
unit == null || UNITS_WITH_DAY_SET.has(unit as UNITS_WITH_DAY_TYPE);
export const hasHour = (unit: DatetimeUnit) =>
unit == null || UNITS_WITH_HOUR_SET.has(unit as UNITS_WITH_HOUR_TYPE);
export function getTimeFormatFromStyle(
style: string,
unit: DatetimeUnit,
timeEnabled?: "minutes" | "milliseconds" | "seconds" | null,
) {
const format = style;
if (!timeEnabled || timeEnabled === "milliseconds") {
return format.replace(/mm/, "mm:ss.SSS");
} else if (timeEnabled === "seconds") {
return format.replace(/mm/, "mm:ss");
} else {
return format;
}
}
import React from "react";
import ExternalLink from "metabase/core/components/ExternalLink";
import { renderLinkTextForClick } from "./link";
import { getDataFromClicked } from "metabase/lib/click-behavior";
import { OptionsType } from "./types";
// https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L27
const EMAIL_ALLOW_LIST_REGEX =
/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
export function formatEmail(
value: string,
{ jsx, rich, view_as = "auto", link_text, clicked }: OptionsType = {},
) {
const email = String(value);
const label =
clicked && link_text
? renderLinkTextForClick(link_text, getDataFromClicked(clicked))
: null;
if (
jsx &&
rich &&
(view_as === "email_link" || view_as === "auto") &&
EMAIL_ALLOW_LIST_REGEX.test(email)
) {
return (
<ExternalLink href={"mailto:" + email}>{label || email}</ExternalLink>
);
} else {
return email;
}
}
import { Field } from "metabase-types/api/field";
export function formatField(field: Field) {
if (!field) {
return "";
}
return field.dimensions?.name || field.display_name || field.name;
}
import d3 from "d3";
import { isLatitude, isLongitude } from "metabase/lib/schema_metadata";
import { decimalCount } from "metabase/visualizations/lib/numeric";
import { OptionsType } from "./types";
const DECIMAL_DEGREES_FORMATTER = d3.format(".08f");
const DECIMAL_DEGREES_FORMATTER_COMPACT = d3.format(".02f");
const BINNING_DEGREES_FORMATTER = (value: number, binWidth: number) => {
return d3.format(`.0${decimalCount(binWidth)}f`)(value);
};
export function formatCoordinate(value: number, options: OptionsType = {}) {
const binWidth = options.column?.binning_info?.bin_width;
let direction = "";
if (isLatitude(options.column)) {
direction = " " + (value < 0 ? "S" : "N");
value = Math.abs(value);
} else if (isLongitude(options.column)) {
direction = " " + (value < 0 ? "W" : "E");
value = Math.abs(value);
}
const formattedValue = binWidth
? BINNING_DEGREES_FORMATTER(value, binWidth)
: options.compact
? DECIMAL_DEGREES_FORMATTER_COMPACT(value)
: DECIMAL_DEGREES_FORMATTER(value);
return formattedValue + "°" + direction;
}
import React from "react";
import { getUrlProtocol } from "./url";
import { OptionsType } from "./types";
export function formatImage(
value: string,
{ jsx, rich, view_as = "auto", link_text }: OptionsType = {},
) {
const url = String(value);
const protocol = getUrlProtocol(url);
const acceptedProtocol = protocol === "http:" || protocol === "https:";
if (jsx && rich && view_as === "image" && acceptedProtocol) {
return <img src={url} style={{ height: 30 }} />;
} else {
return url;
}
}
import React from "react";
import d3 from "d3";
import Humanize from "humanize-plus";
import { COMPACT_CURRENCY_OPTIONS, getCurrencySymbol } from "./currency";
const DISPLAY_COMPACT_DECIMALS_CUTOFF = 1000;
const FIXED_NUMBER_FORMATTER = d3.format(",.f");
const PRECISION_NUMBER_FORMATTER = d3.format(".2f");
interface FormatNumberOptionsType {
_numberFormatter?: any;
compact?: boolean;
currency?: string;
currency_in_header?: boolean;
currency_style?: string;
decimals?: string | number;
jsx?: any;
maximumFractionDigits?: number;
minimumFractionDigits?: number;
minimumIntegerDigits?: number;
maximumSignificantDigits?: number;
minimumSignificantDigits?: number;
negativeInParentheses?: boolean;
number_separators?: string;
number_style?: string;
scale?: string;
type?: string;
}
interface DEFAULT_NUMBER_OPTIONS_TYPE {
compact: boolean;
maximumFractionDigits: number;
minimumFractionDigits?: number;
}
const DEFAULT_NUMBER_OPTIONS: DEFAULT_NUMBER_OPTIONS_TYPE = {
compact: false,
maximumFractionDigits: 2,
};
// for extracting number portion from a formatted currency string
//
// NOTE: match minus/plus and number separately to handle interposed currency symbol -$1.23
const NUMBER_REGEX = /([\+\-])?[^0-9]*([0-9\., ]+)/;
const DEFAULT_NUMBER_SEPARATORS = ".,";
function getDefaultNumberOptions(options: { decimals?: string | number }) {
const defaults = { ...DEFAULT_NUMBER_OPTIONS };
// decimals sets the exact number of digits after the decimal place
if (typeof options.decimals === "number" && !isNaN(options.decimals)) {
defaults.minimumFractionDigits = options.decimals;
defaults.maximumFractionDigits = options.decimals;
}
return defaults;
}
export function formatNumber(
number: number,
options: FormatNumberOptionsType = {},
): any {
options = { ...getDefaultNumberOptions(options), ...options };
if (typeof options.scale === "number" && !isNaN(options.scale)) {
number = options.scale * number;
}
if (number < 0 && options.negativeInParentheses) {
return (
"(" +
formatNumber(-number, { ...options, negativeInParentheses: false }) +
")"
);
}
if (options.compact) {
return formatNumberCompact(number, options);
} else if (options.number_style === "scientific") {
return formatNumberScientific(number, options);
} else {
try {
let nf;
if (number < 1 && number > -1 && options.decimals == null) {
// NOTE: special case to match existing behavior for small numbers, use
// max significant digits instead of max fraction digits
nf = numberFormatterForOptions({
...options,
maximumSignificantDigits: Math.max(
2,
options.minimumSignificantDigits || 0,
),
maximumFractionDigits: undefined,
});
} else if (options._numberFormatter) {
// NOTE: options._numberFormatter allows you to provide a predefined
// Intl.NumberFormat object for increased performance
nf = options._numberFormatter;
} else {
nf = numberFormatterForOptions(options);
}
let formatted = nf.format(number);
// extract number portion of currency if we're formatting a cell
if (
options["type"] === "cell" &&
options["currency_in_header"] &&
options["number_style"] === "currency"
) {
const match = formatted.match(NUMBER_REGEX);
if (match) {
formatted = (match[1] || "").trim() + (match[2] || "").trim();
}
}
// replace the separators if not default
const separators = options["number_separators"];
if (separators && separators !== DEFAULT_NUMBER_SEPARATORS) {
formatted = replaceNumberSeparators(formatted, separators);
}
// fixes issue where certain symbols, such as
// czech Kč, and Bitcoin ₿, are not displayed
if (options["currency_style"] === "symbol") {
formatted = formatted.replace(
options["currency"],
getCurrencySymbol(options["currency"] as string),
);
}
return formatted;
} catch (e) {
console.warn("Error formatting number", e);
// fall back to old, less capable formatter
// NOTE: does not handle things like currency, percent
return FIXED_NUMBER_FORMATTER(
d3.round(number, options.maximumFractionDigits),
);
}
}
}
export function numberFormatterForOptions(options: FormatNumberOptionsType) {
options = { ...getDefaultNumberOptions(options), ...options };
// always use "en" locale so we have known number separators we can replace depending on number_separators option
// TODO: if we do that how can we get localized currency names?
return new Intl.NumberFormat("en", {
style: options.number_style,
currency: options.currency,
currencyDisplay: options.currency_style,
// always use grouping separators, but we may replace/remove them depending on number_separators option
useGrouping: true,
minimumIntegerDigits: options.minimumIntegerDigits,
minimumFractionDigits: options.minimumFractionDigits,
maximumFractionDigits: options.maximumFractionDigits,
minimumSignificantDigits: options.minimumSignificantDigits,
maximumSignificantDigits: options.maximumSignificantDigits,
}) as any;
}
function formatNumberCompact(value: number, options: FormatNumberOptionsType) {
if (options.number_style === "percent") {
return formatNumberCompactWithoutOptions(value * 100) + "%";
}
if (options.number_style === "currency") {
try {
const nf = numberFormatterForOptions({
...options,
...COMPACT_CURRENCY_OPTIONS,
});
if (Math.abs(value) < DISPLAY_COMPACT_DECIMALS_CUTOFF) {
return nf.format(value);
}
const { value: currency } = nf
.formatToParts(value)
.find((p: any) => p.type === "currency");
return currency + formatNumberCompactWithoutOptions(value);
} catch (e) {
// Intl.NumberFormat failed, so we fall back to a non-currency number
return formatNumberCompactWithoutOptions(value);
}
}
if (options.number_style === "scientific") {
return formatNumberScientific(value, {
...options,
// unsetting maximumFractionDigits prevents truncation of small numbers
maximumFractionDigits: undefined,
minimumFractionDigits: 1,
});
}
return formatNumberCompactWithoutOptions(value);
}
function formatNumberCompactWithoutOptions(value: number) {
if (value === 0) {
// 0 => 0
return "0";
} else if (Math.abs(value) < DISPLAY_COMPACT_DECIMALS_CUTOFF) {
// 0.1 => 0.1
return PRECISION_NUMBER_FORMATTER(value).replace(/\.?0+$/, "");
} else {
// 1 => 1
// 1000 => 1K
return Humanize.compactInteger(Math.round(value), 1);
}
}
// replaces the decimale and grouping separators with those specified by a NumberSeparators option
function replaceNumberSeparators(formatted: any, separators: any) {
const [decimalSeparator, groupingSeparator] = (separators || ".,").split("");
const separatorMap = {
",": groupingSeparator || "",
".": decimalSeparator,
};
return formatted.replace(
/,|\./g,
(separator: "." | ",") => separatorMap[separator],
);
}
function formatNumberScientific(
value: number,
options: FormatNumberOptionsType,
) {
if (options.maximumFractionDigits) {
value = d3.round(value, options.maximumFractionDigits);
}
const exp = replaceNumberSeparators(
value.toExponential(options.minimumFractionDigits),
options?.number_separators,
);
if (options.jsx) {
const [m, n] = exp.split("e");
return (
<span>
{m}×10<sup>{n.replace(/^\+/, "")}</sup>
</span>
);
} else {
return exp;
}
}
export function formatSQL(sql: string) {
if (typeof sql === "string") {
sql = sql.replace(/\sFROM/, "\nFROM");
sql = sql.replace(/\sLEFT JOIN/, "\nLEFT JOIN");
sql = sql.replace(/\sWHERE/, "\nWHERE");
sql = sql.replace(/\sGROUP BY/, "\nGROUP BY");
sql = sql.replace(/\sORDER BY/, "\nORDER BY");
sql = sql.replace(/\sLIMIT/, "\nLIMIT");
sql = sql.replace(/\sAND\s/, "\n AND ");
sql = sql.replace(/\sOR\s/, "\n OR ");
return sql;
}
}
import inflection from "inflection";
import { getDataFromClicked } from "metabase/lib/click-behavior";
import { formatUrl } from "./url";
import { renderLinkTextForClick } from "./link";
import { formatValue, getRemappedValue } from "./value";
import { formatEmail } from "./email";
import { formatImage } from "./image";
import type { OptionsType } from "./types";
export function singularize(str: string, singular?: string) {
return inflection.singularize(str, singular);
}
export function pluralize(str: string, plural?: string) {
return inflection.pluralize(str, plural);
}
export function capitalize(str: string, { lowercase = true } = {}) {
const firstChar = str.charAt(0).toUpperCase();
let rest = str.slice(1);
if (lowercase) {
rest = rest.toLowerCase();
}
return firstChar + rest;
}
export function inflect(
str: string,
count: number,
singular: string | undefined,
plural: string | undefined,
) {
return inflection.inflect(str, count, singular, plural);
}
export function titleize(str: string) {
return inflection.titleize(str);
}
export function humanize(str: string, lowFirstLetter?: boolean) {
return inflection.humanize(str, lowFirstLetter);
}
// fallback for formatting a string without a column semantic_type
export function formatStringFallback(value: any, options: OptionsType = {}) {
if (options.view_as !== null) {
value = formatUrl(value, options);
if (typeof value === "string") {
value = formatEmail(value, options);
}
if (typeof value === "string") {
value = formatImage(value, options);
}
}
return value;
}
export function conjunct(list: string[], conjunction: string) {
return (
list.slice(0, -1).join(`, `) +
(list.length > 2 ? `,` : ``) +
(list.length > 1 ? ` ${conjunction} ` : ``) +
(list[list.length - 1] || ``)
);
}
// Removes trailing "id" from field names
export function stripId(name: string) {
return name?.replace(/ id$/i, "").trim();
}
function getLinkText(value: string, options: OptionsType) {
const { view_as, link_text, clicked } = options;
const isExplicitLink = view_as === "link";
const hasCustomizedText = link_text && clicked;
if (isExplicitLink && hasCustomizedText) {
return renderLinkTextForClick(link_text, getDataFromClicked(clicked));
}
return (
getRemappedValue(value, options) ||
formatValue(value, { ...options, view_as: null })
);
}
import { msgid, ngettext } from "ttag";
import { parseTime, parseTimestamp } from "metabase/lib/time";
import { Moment } from "moment-timezone";
import {
DEFAULT_TIME_STYLE,
DEFAULT_DATE_STYLE,
getTimeFormatFromStyle,
hasHour,
} from "./datetime-utils";
import type { Value } from "metabase-types/types/Dataset";
import type { DatetimeUnit } from "metabase-types/api/query";
export function duration(milliseconds: number) {
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
if (milliseconds >= HOUR) {
const hours = Math.round(milliseconds / HOUR);
return ngettext(msgid`${hours} hour`, `${hours} hours`, hours);
}
if (milliseconds >= MINUTE) {
const minutes = Math.round(milliseconds / MINUTE);
return ngettext(msgid`${minutes} minute`, `${minutes} minutes`, minutes);
}
const seconds = Math.round(milliseconds / SECOND);
return ngettext(msgid`${seconds} second`, `${seconds} seconds`, seconds);
}
export function formatTime(time: Moment) {
const parsedTime = parseTime(time);
return parsedTime.isValid() ? parsedTime.format("LT") : String(time);
}
interface TimeWithUnitType {
local?: boolean;
time_enabled?: "minutes" | "milliseconds" | "seconds" | boolean;
time_format?: string;
time_style?: string;
}
export function formatTimeWithUnit(
value: number,
unit: DatetimeUnit,
options: TimeWithUnitType = {},
) {
const m = parseTimestamp(value, unit, options.local);
if (!m.isValid()) {
return String(value);
}
const timeStyle = options.time_style
? options.time_style
: DEFAULT_TIME_STYLE;
const timeEnabled = options.time_enabled
? options.time_enabled
: hasHour(unit)
? "minutes"
: null;
const timeFormat = options.time_format
? options.time_format
: getTimeFormatFromStyle(timeStyle, unit, timeEnabled as any);
return m.format(timeFormat);
}
export interface OptionsType {
click_behavior?: any;
clicked?: any;
column?: any;
compact?: boolean;
jsx?: boolean;
link_text?: string;
link_url?: string;
majorWidth?: number;
markdown_template?: any;
maximumFractionDigits?: number;
noRange?: boolean;
prefix?: string;
remap?: any;
rich?: boolean;
suffix?: string;
type?: string;
view_as?: string | null;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment