diff --git a/addons/spreadsheet/static/src/helpers/constants.js b/addons/spreadsheet/static/src/helpers/constants.js index cb766da96f2..2314683f497 100644 --- a/addons/spreadsheet/static/src/helpers/constants.js +++ b/addons/spreadsheet/static/src/helpers/constants.js @@ -4,14 +4,6 @@ import { _t } from "@web/core/l10n/translation"; export const DEFAULT_LINES_NUMBER = 20; -export const FORMATS = { - day: { out: "MM/DD/YYYY", display: "DD MMM YYYY", interval: "d" }, - week: { out: "ww/YYYY", display: "[W]w YYYY", interval: "w" }, - month: { out: "MM/YYYY", display: "MMMM YYYY", interval: "M" }, - quarter: { out: "Q/YYYY", display: "[Q]Q YYYY", interval: "Q" }, - year: { out: "YYYY", display: "YYYY", interval: "y" }, -}; - export const HEADER_STYLE = { fillColor: "#E6F2F3" }; export const TOP_LEVEL_STYLE = { bold: true, fillColor: "#E6F2F3" }; export const MEASURE_STYLE = { fillColor: "#E6F2F3", textColor: "#756f6f" }; diff --git a/addons/spreadsheet/static/src/pivot/pivot_data_source.js b/addons/spreadsheet/static/src/pivot/pivot_data_source.js index 8df62a395ce..307480437a5 100644 --- a/addons/spreadsheet/static/src/pivot/pivot_data_source.js +++ b/addons/spreadsheet/static/src/pivot/pivot_data_source.js @@ -141,11 +141,12 @@ export default class PivotDataSource extends OdooViewsDataSource { /** * @param {string} fieldName * @param {string} value raw string value + * @param {object} locale * @returns {string} */ - getGroupByDisplayLabel(fieldName, value) { + getGroupByDisplayLabel(fieldName, value, locale) { this._assertDataIsLoaded(); - return this._model.getGroupByDisplayLabel(fieldName, value); + return this._model.getGroupByDisplayLabel(fieldName, value, locale); } /** diff --git a/addons/spreadsheet/static/src/pivot/pivot_functions.js b/addons/spreadsheet/static/src/pivot/pivot_functions.js index 52f37857a00..b695af0266f 100644 --- a/addons/spreadsheet/static/src/pivot/pivot_functions.js +++ b/addons/spreadsheet/static/src/pivot/pivot_functions.js @@ -98,7 +98,7 @@ functionRegistry const args = domain.map(toString); assertPivotsExists(pivotId, this.getters); assertDomainLength(args); - return this.getters.getDisplayedPivotHeaderValue(pivotId, args); + return this.getters.getDisplayedPivotHeaderValue(pivotId, args, this.locale); }, computeFormat: function (pivotId, ...domain) { pivotId = toString(pivotId.value); @@ -121,10 +121,18 @@ functionRegistry return "#,##0.00"; case "date": case "datetime": - if (aggregateOperator === "day") { - return this.locale.dateFormat; + switch (aggregateOperator) { + case "day": + return this.locale.dateFormat; + case "month": + return "mmmm yyyy"; + case "year": + return "0"; + case "week": + case "quarter": + return undefined; } - return undefined; + break; default: return undefined; } diff --git a/addons/spreadsheet/static/src/pivot/pivot_helpers.js b/addons/spreadsheet/static/src/pivot/pivot_helpers.js index 2236e1077c9..d0b48327bec 100644 --- a/addons/spreadsheet/static/src/pivot/pivot_helpers.js +++ b/addons/spreadsheet/static/src/pivot/pivot_helpers.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import { _t } from "@web/legacy/js/services/core"; -import { FORMATS } from "../helpers/constants"; import { getOdooFunctions } from "../helpers/odoo_functions_helpers"; export const pivotFormulaRegex = /^=.*PIVOT/; @@ -10,19 +9,6 @@ export const pivotFormulaRegex = /^=.*PIVOT/; // Public //-------------------------------------------------------------------------- -/** - * Format a data - * - * @param {string} interval aggregate interval i.e. month, week, quarter, ... - * @param {string} value - */ -export function formatDate(interval, value) { - const output = FORMATS[interval].display; - const input = FORMATS[interval].out; - const date = moment(value, input); - return date.isValid() ? date.format(output) : _t("None"); -} - /** * Parse a spreadsheet formula and detect the number of PIVOT functions that are * present in the given formula. diff --git a/addons/spreadsheet/static/src/pivot/pivot_model.js b/addons/spreadsheet/static/src/pivot/pivot_model.js index 54da5cbc0da..72f3cc6cb4d 100644 --- a/addons/spreadsheet/static/src/pivot/pivot_model.js +++ b/addons/spreadsheet/static/src/pivot/pivot_model.js @@ -5,14 +5,11 @@ import { Domain } from "@web/core/domain"; import { sprintf } from "@web/core/utils/strings"; import { PivotModel } from "@web/views/pivot/pivot_model"; import { computeReportMeasures } from "@web/views/utils"; -import { session } from "@web/session"; - -import { FORMATS } from "../helpers/constants"; import * as spreadsheet from "@odoo/o-spreadsheet"; -import { formatDate } from "./pivot_helpers"; import { PERIODS } from "@spreadsheet/pivot/pivot_helpers"; import { SpreadsheetPivotTable } from "@spreadsheet/pivot/pivot_table"; +import { pivotTimeAdapter } from "./pivot_time_adapters"; const { toString, toNumber, toBoolean } = spreadsheet.helpers; const { DEFAULT_LOCALE } = spreadsheet.constants; @@ -81,9 +78,10 @@ function throwUnsupportedFieldError(field) { * the two group values are "42" and "won". * @param {object} field * @param {number | boolean | string} groupValue + * @param {"day" | "week" | "month" | "quarter" | "year" | undefined} aggregateOperator * @returns {number | boolean | string} */ -export function parsePivotFormulaFieldValue(field, groupValue) { +export function toNormalizedPivotValue(field, groupValue, aggregateOperator) { const groupValueString = typeof groupValue === "boolean" ? toString(groupValue).toLocaleLowerCase() @@ -98,7 +96,10 @@ export function parsePivotFormulaFieldValue(field, groupValue) { switch (field.type) { case "datetime": case "date": - return toString(groupValueString); + return pivotTimeAdapter(aggregateOperator).normalizeFunctionValue( + groupValueString, + field + ); case "selection": case "char": case "text": @@ -299,7 +300,7 @@ export class SpreadsheetPivotModel extends PivotModel { * @param {string} groupValueString Value of the group by * @returns {string} */ - getGroupByDisplayLabel(groupFieldString, groupValueString) { + getGroupByDisplayLabel(groupFieldString, groupValueString, locale = DEFAULT_LOCALE) { if (groupValueString === NO_RECORD_AT_THIS_POSITION) { return ""; } @@ -311,13 +312,15 @@ export class SpreadsheetPivotModel extends PivotModel { return this.parseGroupField(groupValueString).field.string; } const { field, aggregateOperator } = this.parseGroupField(groupFieldString); - const value = parsePivotFormulaFieldValue(field, groupValueString); + const value = toNormalizedPivotValue(field, groupValueString, aggregateOperator); const undef = _t("None"); if (this._isDateField(field)) { + // TODO include this parsing to the pivot time adapters and extend it to other time periods if (value && aggregateOperator === "day") { return toNumber(value, DEFAULT_LOCALE); } - return formatDate(aggregateOperator, value); + const adapter = pivotTimeAdapter(aggregateOperator); + return adapter.format(value, locale); } if (field.relation) { const label = this.metadataRepository.getRecordDisplayName(field.relation, value); @@ -455,39 +458,16 @@ export class SpreadsheetPivotModel extends PivotModel { return groupBys.map((groupBy) => { const { field, aggregateOperator } = this.parseGroupField(groupBy); if (this._isDateField(field)) { - const value = this._getGroupStartingDay(groupBy, group); - if (!value) { - return false; - } - const fOut = FORMATS[aggregateOperator]["out"]; - // eslint-disable-next-line no-undef - const date = moment(value); - return date.isValid() ? date.format(fOut) : false; + return pivotTimeAdapter(aggregateOperator).normalizeServerValue( + groupBy, + field, + group + ); } return this._sanitizeValue(group[groupBy]); }); } - /** - * When grouping by a time field, return - * the group starting day (local to the timezone) - * @param {string} groupBy - * @param {object} readGroup - * @returns {string | undefined} - */ - _getGroupStartingDay(groupBy, readGroup) { - if (!readGroup["__range"] || !readGroup["__range"][groupBy]) { - return undefined; - } - const { field } = this.parseGroupField(groupBy); - const sqlValue = readGroup["__range"][groupBy].from; - if (this.metaData.fields[field.name].type === "date") { - return sqlValue; - } - const userTz = session.user_context.tz || luxon.Settings.defaultZoneName; - return luxon.DateTime.fromSQL(sqlValue, { zone: "utc" }).setZone(userTz).toISODate(); - } - /** * Check if the given field is used as col group by */ @@ -555,12 +535,13 @@ export class SpreadsheetPivotModel extends PivotModel { while (i < domain.length) { const groupFieldString = domain[i]; const groupValue = domain[i + 1]; - const { field, isPositional } = this.parseGroupField(groupFieldString); + const { field, isPositional, aggregateOperator } = + this.parseGroupField(groupFieldString); let value; if (isPositional) { value = this._parsePivotFormulaWithPosition(field, groupValue, cols, rows); } else { - value = parsePivotFormulaFieldValue(field, groupValue); + value = toNormalizedPivotValue(field, groupValue, aggregateOperator); } if (this._isCol(field)) { cols.push(value); diff --git a/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js b/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js new file mode 100644 index 00000000000..30d2622093f --- /dev/null +++ b/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js @@ -0,0 +1,274 @@ +/** @odoo-module */ + +import { helpers, constants } from "@odoo/o-spreadsheet"; +import { deserializeDate } from "@web/core/l10n/dates"; +import { _t } from "@web/core/l10n/translation"; +import { sprintf } from "@web/core/utils/strings"; +import { session } from "@web/session"; +const { toNumber, formatValue } = helpers; +const { DEFAULT_LOCALE } = constants; + +const { DateTime } = luxon; + +/** + * @param {"day" | "week" | "month" | "quarter" | "month"} groupAggregate + * @returns {PivotTimeAdapter} + */ +export function pivotTimeAdapter(groupAggregate) { + return TIME_ADAPTERS[groupAggregate]; +} + +/** + * The Time Adapter: Managing Time Periods for Pivot Functions + * + * Overview: + * A time adapter is responsible for managing time periods associated with pivot functions. + * Each type of period (day, week, month, quarter, etc.) has its own dedicated adapter. + * The adapter's primary role is to normalize period values between spreadsheet functions, + * the server, and the datasource. + * By normalizing the period value, it can be stored consistently in the datasource. + * + * Normalization Process: + * When dealing with the server, the time adapter ensures that the received periods are + * normalized before being stored in the datasource. + * For example, if the server returns a day period as "2023-12-25 22:00:00," the time adapter + * transforms it into the normalized form "12/25/2023" for storage in the datasource. + * + * Similarly, when working with functions in the spreadsheet, the time adapter normalizes + * the provided period to facilitate accurate lookup of values in the datasource. + * For instance, if the spreadsheet function represents a day period as a number generated + * by the DATE function (DATE(2023, 12, 25)), the time adapter will normalize it accordingly. + * + * Example: + * To illustrate the normalization process, let's consider the day period: + * + * 1. The server returns a day period as "2023-12-25 22:00:00" + * 2. The time adapter normalizes this period to "12/25/2023" for storage in the datasource. + * 3. Meanwhile, the spreadsheet function represents the day period as a number obtained from + * the DATE function (DATE(2023, 12, 25)). + * + * By applying the appropriate normalization, the time adapter ensures that the periods from + * different sources are consistently represented and can be effectively utilized for lookup + * operations in the datasource. + * + * Implementation notes/tips: + * - Do not mix luxon and spreadsheet dates in the same function. Timezones are not handled the same way. + * Spreadsheet dates are naive dates (no timezone) while luxon dates are timezone aware dates. + * **Don't do this**: DateTime.fromJSDate(toJsDate(value)) (it will be interpreted as UTC) + * + * - spreadsheet formats and luxon formats are not the same but can be equivalent. + * For example: "MM/dd/yyyy" (luxon format) is equivalent to "mm/dd/yyyy" (spreadsheet format) + * + * Limitations: + * If a period value is provided as a **string** to a function, it will interpreted as being in the default locale. + * e.g. in `ODOO.PIVOT(1, "amount", "create_date", "1/5/2023")`, the day is interpreted as being the 5th of January 2023, + * even if the spreadsheet locale is set to French and such a date is usually interpreted as the 1st of May 2023. + * The reason is ODOO.PIVOT functions are currently generated without being aware of the spreadsheet locale. + * + * @typedef {Object} PivotTimeAdapter + * @property {(groupBy: string, field: string, readGroupResult: object) => string} normalizeServerValue + * @property {(value: string) => string} normalizeFunctionValue + * @property {(normalizedValue: string, step: number) => string} increment + * @property {(normalizedValue: string, locale: Object) => string} format + */ + +/** + * @type {PivotTimeAdapter} + * Normalized value: "12/25/2023" + * + * Note: Those two format are equivalent: + * - "MM/dd/yyyy" (luxon format) + * - "mm/dd/yyyy" (spreadsheet format) + **/ +const dayAdapter = { + normalizeServerValue(groupBy, field, readGroupResult) { + const serverDayValue = getGroupStartingDay(field, groupBy, readGroupResult); + const date = deserializeDate(serverDayValue); + return date.toFormat("MM/dd/yyyy"); + }, + normalizeFunctionValue(value) { + const date = toNumber(value, DEFAULT_LOCALE); + return formatValue(date, { locale: DEFAULT_LOCALE, format: "mm/dd/yyyy" }); + }, + increment(normalizedValue, step) { + const date = DateTime.fromFormat(normalizedValue, "MM/dd/yyyy"); + return date.plus({ days: step }).toFormat("MM/dd/yyyy"); + }, + format(normalizedValue, locale) { + const value = toNumber(normalizedValue, DEFAULT_LOCALE); + return formatValue(value, { locale, format: locale.dateFormat }); + }, +}; + +/** + * @type {PivotTimeAdapter} + * Normalized value: "2/2023" for week 2 of 2023 + */ +const weekAdapter = { + normalizeServerValue(groupBy, field, readGroupResult) { + const weekValue = readGroupResult[groupBy]; + const { week, year } = parseServerWeekHeader(weekValue); + return `${week}/${year}`; + }, + normalizeFunctionValue(value) { + const [week, year] = value.split("/"); + return `${Number(week)}/${Number(year)}`; + }, + increment(normalizedValue, step) { + const [week, year] = normalizedValue.split("/"); + const weekNumber = Number(week); + const yearNumber = Number(year); + const date = DateTime.fromObject({ weekNumber, weekYear: yearNumber }); + const nextWeek = date.plus({ weeks: step }); + return `${nextWeek.weekNumber}/${nextWeek.weekYear}`; + }, + format(normalizedValue, locale) { + const [year, week] = normalizedValue.split("/"); + return sprintf(_t("W%(week)s %(year)s"), { week, year }); + }, +}; + +/** + * @type {PivotTimeAdapter} + * normalized month value is a string formatted as "MM/yyyy" (luxon format) + * e.g. "01/2020" for January 2020 + */ +const monthAdapter = { + normalizeServerValue(groupBy, field, readGroupResult) { + const firstOfTheMonth = getGroupStartingDay(field, groupBy, readGroupResult); + const date = deserializeDate(firstOfTheMonth); + return date.toFormat("MM/yyyy"); + }, + normalizeFunctionValue(value) { + const date = toNumber(value, DEFAULT_LOCALE); + return formatValue(date, { DEFAULT_LOCALE, format: "mm/yyyy" }); + }, + increment(normalizedValue, step) { + return DateTime.fromFormat(normalizedValue, "MM/yyyy") + .plus({ months: step }) + .toFormat("MM/yyyy"); + }, + format(normalizedValue, locale) { + const value = toNumber(normalizedValue, DEFAULT_LOCALE); + return formatValue(value, { locale, format: "mmmm yyyy" }); + }, +}; + +/** + * @type {PivotTimeAdapter} + * normalized quarter value is "quarter/year" + * e.g. "1/2020" for Q1 2020 + */ +const quarterAdapter = { + normalizeServerValue(groupBy, field, readGroupResult) { + const firstOfTheQuarter = getGroupStartingDay(field, groupBy, readGroupResult); + const date = deserializeDate(firstOfTheQuarter); + return `${date.quarter}/${date.year}`; + }, + normalizeFunctionValue(value) { + const [quarter, year] = value.split("/"); + return `${quarter}/${year}`; + }, + increment(normalizedValue, step) { + const [quarter, year] = normalizedValue.split("/"); + const date = DateTime.fromObject({ year: Number(year), month: Number(quarter) * 3 }); + const nextQuarter = date.plus({ quarters: step }); + return `${nextQuarter.quarter}/${nextQuarter.year}`; + }, + format(normalizedValue, locale) { + const [quarter, year] = normalizedValue.split("/"); + return sprintf(_t("Q%(quarter)s %(year)s"), { quarter, year }); + }, +}; +/** + * @type {PivotTimeAdapter} + */ +const yearAdapter = { + normalizeServerValue(groupBy, field, readGroupResult) { + return Number(readGroupResult[groupBy]); + }, + normalizeFunctionValue(value) { + return toNumber(value, DEFAULT_LOCALE); + }, + increment(normalizedValue, step) { + return normalizedValue + step; + }, + format(normalizedValue, locale) { + return formatValue(normalizedValue, { locale, format: "0" }); + }, +}; + +/** + * Decorate adapter functions to handle the empty value "false" + * @param {PivotTimeAdapter} adapter + */ +function falseHandlerDecorator(adapter) { + return { + normalizeServerValue(groupBy, field, readGroupResult) { + if (readGroupResult[groupBy] === false) { + return false; + } + return adapter.normalizeServerValue(groupBy, field, readGroupResult); + }, + normalizeFunctionValue(value) { + if (value === false || value === "false") { + return false; + } + return adapter.normalizeFunctionValue(value); + }, + increment(normalizedValue, step) { + if (normalizedValue === false) { + return false; + } + return adapter.increment(normalizedValue, step); + }, + format(normalizedValue, locale) { + if (normalizedValue === false) { + return _t("None"); + } + return adapter.format(normalizedValue, locale); + }, + }; +} + +const TIME_ADAPTERS = { + day: falseHandlerDecorator(dayAdapter), + week: falseHandlerDecorator(weekAdapter), + month: falseHandlerDecorator(monthAdapter), + quarter: falseHandlerDecorator(quarterAdapter), + year: falseHandlerDecorator(yearAdapter), +}; + +/** + * When grouping by a time field, return + * the group starting day (local to the timezone) + * @param {object} field + * @param {string} groupBy + * @param {object} readGroup + * @returns {string | undefined} + */ +function getGroupStartingDay(field, groupBy, readGroup) { + if (!readGroup["__range"] || !readGroup["__range"][groupBy]) { + return undefined; + } + const sqlValue = readGroup["__range"][groupBy].from; + if (field.type === "date") { + return sqlValue; + } + const userTz = session.user_context.tz || luxon.Settings.defaultZoneName; + return DateTime.fromSQL(sqlValue, { zone: "utc" }).setZone(userTz).toISODate(); +} + +/** + * Parses a pivot week header value. + * @param {string} value + * @example + * parseServerWeekHeader("W1 2020") // { week: 1, year: 2020 } + */ +function parseServerWeekHeader(value) { + // Value is always formatted as "W1 2020", no matter the language. + // Parsing this formatted value is the only way to ensure we get the same + // locale aware week number as the one used in the server. + const [week, year] = value.split(" "); + return { week: Number(week.slice(1)), year: Number(year) }; +} diff --git a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js index dfb19aa3013..60cbd179049 100644 --- a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js +++ b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js @@ -586,6 +586,20 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { assert.equal(getCellValue(model, "C5"), ""); }); + QUnit.test("PIVOT.HEADER grouped by date field without value", async function (assert) { + for (const interval of ["day", "week", "month", "quarter", "year"]) { + const { model } = await createSpreadsheetWithPivot({ + arch: /* xml */ ` + + + + `, + }); + setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1, "date:${interval}", "false")`); + assert.equal(getCellValue(model, "A1"), "None"); + } + }); + QUnit.test("PIVOT formulas are correctly formatted at evaluation", async function (assert) { const { model } = await createSpreadsheetWithPivot({ arch: /* xml */ ` @@ -615,6 +629,19 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { } ); + QUnit.test("PIVOT.HEADER day are correctly formatted at evaluation", async function (assert) { + const { model } = await createSpreadsheetWithPivot({ + arch: /* xml */ ` + + + + `, + }); + assert.strictEqual(getEvaluatedCell(model, "B1").format, "m/d/yyyy"); + assert.strictEqual(getEvaluatedCell(model, "B1").value, 42474); + assert.strictEqual(getEvaluatedCell(model, "B1").formattedValue, "4/14/2016"); + }); + QUnit.test( "PIVOT.HEADER formulas are correctly formatted at evaluation", async function (assert) { diff --git a/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js b/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js index 386632dcae9..7cf42866ed3 100644 --- a/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js +++ b/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { getFirstPivotFunction, getNumberOfPivotFormulas } from "@spreadsheet/pivot/pivot_helpers"; import { getFirstListFunction, getNumberOfListFormulas } from "@spreadsheet/list/list_helpers"; -import { parsePivotFormulaFieldValue } from "@spreadsheet/pivot/pivot_model"; +import { toNormalizedPivotValue } from "@spreadsheet/pivot/pivot_model"; function stringArg(value) { return { type: "STRING", value: `${value}` }; @@ -27,9 +27,7 @@ QUnit.module("spreadsheet > pivot_helpers", {}, () => { QUnit.test("Extraction with two PIVOT formulas", async function (assert) { const formula = `=ODOO.PIVOT("1", "test") + ODOO.PIVOT("2", "hello", "bla")`; - let functionName; - let args; - ({ functionName, args } = getFirstPivotFunction(formula)); + const { functionName, args } = getFirstPivotFunction(formula); assert.strictEqual(functionName, "ODOO.PIVOT"); assert.strictEqual(args.length, 2); assert.deepEqual(args[0], stringArg("1")); @@ -59,22 +57,22 @@ QUnit.module("spreadsheet > pivot_helpers", {}, () => { }); }); -QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => { +QUnit.module("spreadsheet > toNormalizedPivotValue", {}, () => { QUnit.test("parse values of a selection, char or text field", (assert) => { for (const fieldType of ["selection", "text", "char"]) { const field = { type: fieldType, string: "A field", }; - assert.strictEqual(parsePivotFormulaFieldValue(field, "won"), "won"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), "1"); - assert.strictEqual(parsePivotFormulaFieldValue(field, 1), "1"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "11/2020"), "11/2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), "2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), "01/11/2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, false), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), "true"); + assert.strictEqual(toNormalizedPivotValue(field, "won"), "won"); + assert.strictEqual(toNormalizedPivotValue(field, "1"), "1"); + assert.strictEqual(toNormalizedPivotValue(field, 1), "1"); + assert.strictEqual(toNormalizedPivotValue(field, "11/2020"), "11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "2020"), "2020"); + assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020"), "01/11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "false"), false); + assert.strictEqual(toNormalizedPivotValue(field, false), false); + assert.strictEqual(toNormalizedPivotValue(field, "true"), "true"); } }); @@ -84,16 +82,38 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => { type: fieldType, string: "A field", }; - assert.strictEqual(parsePivotFormulaFieldValue(field, "11/2020"), "11/2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), "2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), "01/11/2020"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), "1"); - assert.strictEqual(parsePivotFormulaFieldValue(field, 1), "1"); - assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, false), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), "true"); // this should throw because it's not a valid date - assert.strictEqual(parsePivotFormulaFieldValue(field, true), "true"); // this should throw because it's not a valid date - assert.strictEqual(parsePivotFormulaFieldValue(field, "won"), "won"); // this should throw because it's not a valid date + // day + assert.strictEqual(toNormalizedPivotValue(field, "1/11/2020", "day"), "01/11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020", "day"), "01/11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "day"), "11/01/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "1", "day"), "12/31/1899"); + assert.strictEqual(toNormalizedPivotValue(field, 1, "day"), "12/31/1899"); + assert.strictEqual(toNormalizedPivotValue(field, "false", "day"), false); + assert.strictEqual(toNormalizedPivotValue(field, false, "day"), false); + // week + assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "week"), "11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "1/2020", "week"), "1/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "01/2020", "week"), "1/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "false", "week"), false); + assert.strictEqual(toNormalizedPivotValue(field, false, "week"), false); + // month + assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "month"), "11/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "1/2020", "month"), "01/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "01/2020", "month"), "01/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "2/11/2020", "month"), "02/2020"); + assert.strictEqual(toNormalizedPivotValue(field, "2/1/2020", "month"), "02/2020"); + assert.strictEqual(toNormalizedPivotValue(field, 1, "month"), "12/1899"); + assert.strictEqual(toNormalizedPivotValue(field, "false", "month"), false); + assert.strictEqual(toNormalizedPivotValue(field, false, "month"), false); + // year + assert.strictEqual(toNormalizedPivotValue(field, "2020", "year"), 2020); + assert.strictEqual(toNormalizedPivotValue(field, 2020, "year"), 2020); + assert.strictEqual(toNormalizedPivotValue(field, "false", "year"), false); + assert.strictEqual(toNormalizedPivotValue(field, false, "year"), false); + + assert.throws(() => toNormalizedPivotValue(field, "true", "month")); + assert.throws(() => toNormalizedPivotValue(field, true, "month")); + assert.throws(() => toNormalizedPivotValue(field, "won", "month")); } }); @@ -102,16 +122,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => { type: "boolean", string: "A field", }; - assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, false), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), true); - assert.strictEqual(parsePivotFormulaFieldValue(field, true), true); - assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "01/11/2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "1")); - assert.throws(() => parsePivotFormulaFieldValue(field, 1)); - assert.throws(() => parsePivotFormulaFieldValue(field, "won")); + assert.strictEqual(toNormalizedPivotValue(field, "false"), false); + assert.strictEqual(toNormalizedPivotValue(field, false), false); + assert.strictEqual(toNormalizedPivotValue(field, "true"), true); + assert.strictEqual(toNormalizedPivotValue(field, true), true); + assert.throws(() => toNormalizedPivotValue(field, "11/2020")); + assert.throws(() => toNormalizedPivotValue(field, "2020")); + assert.throws(() => toNormalizedPivotValue(field, "01/11/2020")); + assert.throws(() => toNormalizedPivotValue(field, "1")); + assert.throws(() => toNormalizedPivotValue(field, 1)); + assert.throws(() => toNormalizedPivotValue(field, "won")); }); QUnit.test("parse values of numeric fields", (assert) => { @@ -120,16 +140,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => { type: fieldType, string: "A field", }; - assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), 2020); - assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), 43841); // a date is actually a number in a spreadsheet - assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), 1); - assert.strictEqual(parsePivotFormulaFieldValue(field, 1), 1); - assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false); - assert.strictEqual(parsePivotFormulaFieldValue(field, false), false); - assert.throws(() => parsePivotFormulaFieldValue(field, "true")); - assert.throws(() => parsePivotFormulaFieldValue(field, true)); - assert.throws(() => parsePivotFormulaFieldValue(field, "won")); - assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020")); + assert.strictEqual(toNormalizedPivotValue(field, "2020"), 2020); + assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020"), 43841); // a date is actually a number in a spreadsheet + assert.strictEqual(toNormalizedPivotValue(field, "11/2020"), 44136); // 1st of november 2020 + assert.strictEqual(toNormalizedPivotValue(field, "1"), 1); + assert.strictEqual(toNormalizedPivotValue(field, 1), 1); + assert.strictEqual(toNormalizedPivotValue(field, "false"), false); + assert.strictEqual(toNormalizedPivotValue(field, false), false); + assert.throws(() => toNormalizedPivotValue(field, "true")); + assert.throws(() => toNormalizedPivotValue(field, true)); + assert.throws(() => toNormalizedPivotValue(field, "won")); } }); @@ -139,16 +159,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => { type: fieldType, string: "A field", }; - assert.throws(() => parsePivotFormulaFieldValue(field, "false")); - assert.throws(() => parsePivotFormulaFieldValue(field, false)); - assert.throws(() => parsePivotFormulaFieldValue(field, "true")); - assert.throws(() => parsePivotFormulaFieldValue(field, true)); - assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "01/11/2020")); - assert.throws(() => parsePivotFormulaFieldValue(field, "1")); - assert.throws(() => parsePivotFormulaFieldValue(field, 1)); - assert.throws(() => parsePivotFormulaFieldValue(field, "won")); + assert.throws(() => toNormalizedPivotValue(field, "false")); + assert.throws(() => toNormalizedPivotValue(field, false)); + assert.throws(() => toNormalizedPivotValue(field, "true")); + assert.throws(() => toNormalizedPivotValue(field, true)); + assert.throws(() => toNormalizedPivotValue(field, "11/2020")); + assert.throws(() => toNormalizedPivotValue(field, "2020")); + assert.throws(() => toNormalizedPivotValue(field, "01/11/2020")); + assert.throws(() => toNormalizedPivotValue(field, "1")); + assert.throws(() => toNormalizedPivotValue(field, 1)); + assert.throws(() => toNormalizedPivotValue(field, "won")); } }); });