From d90f2311636b542b17024ed08d54dd404a9cebd5 Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Tue, 18 Sep 2018 15:40:03 -0700 Subject: [PATCH] Refactor date/time formatting --- frontend/src/metabase/lib/formatting.js | 135 ++++++++---------- frontend/src/metabase/lib/formatting/date.js | 74 ++++++++++ .../visualizations/lib/settings/column.js | 96 ++++++++++--- 3 files changed, 207 insertions(+), 98 deletions(-) create mode 100644 frontend/src/metabase/lib/formatting/date.js diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js index c93f4fe0454..c97dcfd01ca 100644 --- a/frontend/src/metabase/lib/formatting.js +++ b/frontend/src/metabase/lib/formatting.js @@ -25,6 +25,13 @@ import { rangeForValue } from "metabase/lib/dataset"; import { getFriendlyName } from "metabase/visualizations/lib/utils"; import { decimalCount } from "metabase/visualizations/lib/numeric"; +import { + DEFAULT_DATE_STYLE, + getDateFormatFromStyle, + DEFAULT_TIME_STYLE, + getTimeFormatFromStyle, +} from "metabase/lib/formatting/date"; + import Field from "metabase-lib/lib/metadata/Field"; import type { Column, Value } from "metabase/meta/types/Dataset"; import type { DatetimeUnit } from "metabase/meta/types/Query"; @@ -284,27 +291,41 @@ function formatWeek(m: Moment, options: FormattingOptions = {}) { return formatMajorMinor(m.format("wo"), m.format("gggg"), options); } +function replaceDateFormatNames(format, options) { + return format + .replace(/\bMMMM\b/g, getMonthFormat(options)) + .replace(/\bdddd\b/g, getDayFormat(options)); +} + +function formatDateTimeWithFormats(value, dateFormat, timeFormat, options) { + let m = parseTimestamp(value, options.column && options.column.unit); + if (!m.isValid()) { + return String(value); + } + + const format = []; + if (dateFormat) { + format.push(replaceDateFormatNames(dateFormat, options)); + } + if (timeFormat && options.time_enabled) { + format.push(timeFormat); + } + return m.format(format.join(" ")); +} + function formatDateTime(value, options) { let m = parseTimestamp(value, options.column && options.column.unit); if (!m.isValid()) { return String(value); } - if (options.date_format) { - const format = []; - if (options.date_abbreviate) { - format.push( - options.date_format - .replace(/\bMMMM\b/g, getMonthFormat(options)) - .replace(/\bdddd\b/g, getDayFormat(options)), - ); - } else { - format.push(options.date_format); - } - if (options.time_format && options.time_enabled !== false) { - format.push(options.time_format); - } - return m.format(format.join(" ")); + if (options.date_format || options.time_format) { + formatDateTimeWithFormats( + value, + options.date_format, + options.time_format, + options, + ); } else { if (options.show_time === false) { return m.format(options.date_abbreviate ? "ll" : "LL"); @@ -324,71 +345,31 @@ export function formatDateTimeWithUnit( return String(value); } - // only use custom formats for unbucketed dates for now - if (options.date_format) { - formatDateTime(value, options); + // 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); + } } - switch (unit) { - case "hour": // 12 AM - January 1, 2015 - return formatMajorMinor( - m.format("h A"), - m.format(`${getMonthFormat(options)} D, YYYY`), - options, - ); - case "day": // January 1, 2015 - return m.format(`${getMonthFormat(options)} D, YYYY`); - case "week": // 1st - 2015 - if (options.type === "tooltip" && !options.noRange) { - // tooltip show range like "January 1 - 7, 2017" - return formatDateTimeRangeWithUnit(value, unit, options); - } else if (options.type === "cell" && !options.noRange) { - // table cells show range like "Jan 1, 2017 - Jan 7, 2017" - return formatDateTimeRangeWithUnit(value, unit, options); - } else if (options.type === "axis") { - // axis ticks show start of the week as "Jan 1" - return m - .clone() - .startOf(unit) - .format(`MMM D`); - } else { - return formatWeek(m, options); - } - case "month": // January 2015 - return options.jsx ? ( - <div> - <span className="text-bold">{m.format(getMonthFormat(options))}</span>{" "} - {m.format("YYYY")} - </div> - ) : ( - m.format(`${getMonthFormat(options)} YYYY`) - ); - case "year": // 2015 - return m.format("YYYY"); - case "quarter": // Q1 - 2015 - return formatMajorMinor(m.format("[Q]Q"), m.format("YYYY"), { - ...options, - majorWidth: 0, - }); - case "minute-of-hour": - return m.format("m"); - case "hour-of-day": // 12 AM - return m.format("h A"); - case "day-of-week": // Sunday - return m.format(getDayFormat(options)); - case "day-of-month": - return m.format("D"); - case "day-of-year": - return m.format("DDD"); - case "week-of-year": // 1st - return m.format("wo"); - case "month-of-year": // January - return m.format(getMonthFormat(options)); - case "quarter-of-year": // January - return m.format("[Q]Q"); - default: - return formatDateTime(value, options); + let dateFormat = options.date_format; + let timeFormat = options.time_format; + + if (!dateFormat) { + const dateStyle = options.date_style || DEFAULT_DATE_STYLE; + dateFormat = getDateFormatFromStyle(dateStyle, unit); } + + if (!timeFormat) { + const timeStyle = options.time_style || DEFAULT_TIME_STYLE; + timeFormat = getTimeFormatFromStyle(timeStyle, unit, options.time_enabled); + } + + return formatDateTimeWithFormats(value, dateFormat, timeFormat, options); } export function formatTime(value: Value) { diff --git a/frontend/src/metabase/lib/formatting/date.js b/frontend/src/metabase/lib/formatting/date.js new file mode 100644 index 00000000000..ab982d776ac --- /dev/null +++ b/frontend/src/metabase/lib/formatting/date.js @@ -0,0 +1,74 @@ +const DEFAULT_DATE_FORMATS = { + year: "YYYY", + quarter: "[Q]Q - YYYY", + "minute-of-hour": "m", + "hour-of-day": "h A", + "day-of-week": "dddd", + "day-of-month": "D", + "day-of-year": "DDD", + "week-of-year": "wo", + "month-of-year": "MMMM", + "quarter-of-year": "[Q]Q", +}; + +// a "date style" is essentially a "day" format with overrides for larger units +const DATE_STYLE_TO_FORMAT = { + "M/D/YY": { + month: "M/YY", + }, + "D/M/YY": { + month: "M/YY", + }, + "YYYY/M/D": { + month: "YYYY/M", + quarter: "YYYY - [Q]Q", + }, + "MMMM D, YYYY": { + month: "MMMM, YYYY", + }, + "D MMMM, YYYY": { + month: "MMMM, YYYY", + }, + "dddd, MMMM D, YYYY": { + week: "MMMM D, YYYY", + month: "MMMM, YYYY", + }, +}; + +export const DEFAULT_DATE_STYLE = "MMMM D, YYYY"; + +export function getDateFormatFromStyle(style, unit) { + if (DATE_STYLE_TO_FORMAT[style]) { + if (DATE_STYLE_TO_FORMAT[style][unit]) { + return DATE_STYLE_TO_FORMAT[style][unit]; + } + } else { + console.warn("Unknown date style", style); + } + if (DEFAULT_DATE_FORMATS[unit]) { + return DEFAULT_DATE_FORMATS[unit]; + } + return style; +} + +const UNITS_WITH_HOUR = [null, "default", "second", "minute", "hour"]; +const UNITS_WITH_DAY = [...UNITS_WITH_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 => UNITS_WITH_HOUR_SET.has(unit); +export const hasDay = unit => UNITS_WITH_DAY_SET.has(unit); + +export const DEFAULT_TIME_STYLE = "h:mm A"; + +export function getTimeFormatFromStyle(style, unit, timeEnabled) { + let 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; + } +} diff --git a/frontend/src/metabase/visualizations/lib/settings/column.js b/frontend/src/metabase/visualizations/lib/settings/column.js index 72e6c484c12..bd744bee1d8 100644 --- a/frontend/src/metabase/visualizations/lib/settings/column.js +++ b/frontend/src/metabase/visualizations/lib/settings/column.js @@ -8,6 +8,12 @@ import { keyForColumn } from "metabase/lib/dataset"; import { isDate, isNumber, isCoordinate } from "metabase/lib/schema_metadata"; import { getVisualizationRaw } from "metabase/visualizations"; import { numberFormatterForOptions } from "metabase/lib/formatting"; +import { + DEFAULT_DATE_STYLE, + getDateFormatFromStyle, + hasDay, + hasHour, +} from "metabase/lib/formatting/date"; const DEFAULT_GET_COLUMNS = (series, vizSettings) => [].concat(...series.map(s => s.data.cols)); @@ -31,51 +37,99 @@ export function columnSettings({ const EXAMPLE_DATE = moment("2018-01-07 17:24"); -function dateTimeFormatOption(format, description) { +function getDateStyleOptionsForUnit(unit) { + const options = [ + dateStyleOption("M/D/YY", unit, hasDay(unit) && "month, day, year"), + dateStyleOption("D/M/YY", unit, hasDay(unit) && "day, month, year"), + dateStyleOption("YYYY/M/D", unit, hasDay(unit) && "year, month, day"), + dateStyleOption("MMMM D, YYYY", unit), + dateStyleOption("D MMMM, YYYY", unit), + dateStyleOption("dddd, MMMM D, YYYY", unit), + ]; + const seen = new Set(); + return options.filter(option => { + const format = getDateFormatFromStyle(option.value, unit); + if (seen.has(format)) { + return false; + } else { + seen.add(format); + return true; + } + }); +} + +function dateStyleOption(style, unit, description) { + const format = getDateFormatFromStyle(style, unit); + return { + name: + EXAMPLE_DATE.format(format) + (description ? ` (${description})` : ``), + value: style, + }; +} + +function timeStyleOption(style, description) { + const format = style; return { name: EXAMPLE_DATE.format(format) + (description ? ` (${description})` : ``), - value: format, + value: style, }; } export const DATE_COLUMN_SETTINGS = { - date_format: { + date_style: { title: t`Date style`, widget: "radio", - default: "dddd, MMMM D, YYYY", - props: { - options: [ - dateTimeFormatOption("M/D/YYYY", "month, day, year"), - dateTimeFormatOption("D/M/YYYY", "day, month, year"), - dateTimeFormatOption("YYYY/M/D", "year, month, day"), - dateTimeFormatOption("MMMM D, YYYY"), - dateTimeFormatOption("D MMMM YYYY"), - dateTimeFormatOption("dddd, MMMM D, YYYY"), - ], - }, + default: DEFAULT_DATE_STYLE, + getProps: ({ unit }) => ({ + options: getDateStyleOptionsForUnit(unit), + }), + getHidden: ({ unit }) => getDateStyleOptionsForUnit(unit).length < 2, }, date_abbreviate: { title: t`Abbreviate names of days and months`, widget: "toggle", default: false, + getHidden: ({ unit }, settings) => { + const format = getDateFormatFromStyle(settings["date_style"], unit); + return !format.match(/MMMM|dddd/); + }, + readDependencies: ["date_style"], }, time_enabled: { title: t`Show the time`, - widget: "toggle", - default: true, + widget: "buttonGroup", + getProps: ({ unit }, settings) => { + const options = [ + { name: t`Off`, value: null }, + { name: t`Minutes`, value: "minutes" }, + ]; + if (!unit || unit === "default" || unit === "second") { + options.push({ name: t`Seconds`, value: "seconds" }); + } + if (!unit || unit === "default") { + options.push({ name: t`Milliseconds`, value: "milliseconds" }); + } + if (options.length === 2) { + options[1].name = t`On`; + } + return { options }; + }, + getHidden: ({ unit }, settings) => !hasHour(unit), + getDefault: ({ unit }) => (hasHour(unit) ? "minutes" : null), }, - time_format: { + time_style: { title: t`Time style`, widget: "radio", default: "h:mm A", - props: { + getProps: (column, settings) => ({ options: [ - dateTimeFormatOption("h:mm A", "12-hour clock"), - dateTimeFormatOption("k:mm", "24-hour clock"), + timeStyleOption("h:mm A", "12-hour clock"), + timeStyleOption("k:mm", "24-hour clock"), ], - }, + }), getHidden: (column, settings) => !settings["time_enabled"], + readDependencies: ["time_enabled"], }, }; -- GitLab