Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 117 additions & 119 deletions addons/web/static/src/core/l10n/dates.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @odoo-module **/

import { memoize } from "../utils/functions";
import { sprintf } from "../utils/strings";
import { localization } from "./localization";
import { _lt } from "./translation";
Expand All @@ -10,14 +11,20 @@ const { DateTime } = luxon;
* Change the method toJSON to return the formated value to send server side.
*/
DateTime.prototype.toJSON = function () {
return this.setLocale("en").toFormat("yyyy-MM-dd HH:mm:ss");
return this.toFormat("yyyy-MM-dd HH:mm:ss");
};

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

const alphaRegex = /[a-zA-Z]/g;
const nonAlphaRegex = /[^a-zA-Z]/g;
const nonDigitsRegex = /[^0-9]/g;

const normalizeFormatTable = {
// Python strftime to luxon.js conversion table
// See openerp/addons/base/views/res_lang_views.xml
// See odoo/addons/base/views/res_lang_views.xml
// for details about supported directives
a: "ccc",
A: "cccc",
Expand All @@ -40,40 +47,102 @@ const normalizeFormatTable = {
X: "HH:mm:ss",
};

const _normalize_format_cache = {};
const smartDateUnits = {
d: "days",
m: "months",
w: "weeks",
y: "years",
};
const smartDateRegex = new RegExp(`^([+-])(\\d+)([${Object.keys(smartDateUnits).join("")}]?)$`);

/**
* Smart date inputs are shortcuts to write dates quicker.
* These shortcuts should respect the format ^[+-]\d+[dmwy]?$
*
* e.g.
* "+1d" or "+1" will return now + 1 day
* "-2w" will return now - 2 weeks
* "+3m" will return now + 3 months
* "-4y" will return now + 4 years
*
* @param {string} value
* @returns {DateTime|false} Luxon datetime object (in the UTC timezone)
*/
function parseSmartDateInput(value) {
const match = smartDateRegex.exec(value);
if (match) {
let date = DateTime.utc();
const offset = parseInt(match[2], 10);
const unit = smartDateUnits[match[3] || "d"];
if (match[1] === "+") {
date = date.plus({ [unit]: offset });
} else {
date = date.minus({ [unit]: offset });
}
return date;
}
return false;
}

/**
* Enforces some restrictions to a Luxon DateTime object.
* Returns it if within those restrictions.
* Returns false otherwise.
*
* @param {DateTime | false} date
* @returns {DateTime | false}
*/
function constrain(dt) {
let valid = dt !== false;
valid = valid && dt.isValid;
valid = valid && dt.year >= 1000;
valid = valid && dt.year < 10000;
return valid ? dt : false;
}

/**
* Removes any duplicated alphabetic characters in a given string.
* Example: "aa-bb-CCcc-ddD xxxx-Yy-ZZ" -> "a-b-Cc-dD x-Yy-Z"
*
* @param {string} str
* @returns {string}
*/
const stripAlphaDupes = memoize(function _stripAlphaDupes(str) {
return str.replace(alphaRegex, (letter, index, str) => {
return letter === str[index - 1] ? "" : letter;
});
});

/**
* Convert Python strftime to escaped luxon.js format.
*
* @param {string} value original format
* @returns {string} valid Luxon format
*/
export function strftimeToLuxonFormat(value) {
if (_normalize_format_cache[value] === undefined) {
const isletter = /[a-zA-Z]/;
const output = [];
let inToken = false;
for (let index = 0; index < value.length; ++index) {
let character = value[index];
if (character === "%" && !inToken) {
inToken = true;
continue;
}
if (isletter.test(character)) {
if (inToken && normalizeFormatTable[character] !== undefined) {
character = normalizeFormatTable[character];
} else {
character = "[" + character + "]"; // moment.js escape
}
export const strftimeToLuxonFormat = memoize(function _strftimeToLuxonFormat(value) {
const output = [];
let inToken = false;
for (let index = 0; index < value.length; ++index) {
let character = value[index];
if (character === "%" && !inToken) {
inToken = true;
continue;
}
if (character.match(alphaRegex)) {
if (inToken && normalizeFormatTable[character] !== undefined) {
character = normalizeFormatTable[character];
} else {
character = "[" + character + "]"; // moment.js escape
}
output.push(character);
inToken = false;
}
_normalize_format_cache[value] = output.join("");
output.push(character);
inToken = false;
}
return _normalize_format_cache[value];
}
return output.join("");
});

// -----------------------------------------------------------------------------
// Formatting
// -----------------------------------------------------------------------------

/**
Expand Down Expand Up @@ -105,38 +174,28 @@ export function formatDateTime(value, options = {}) {
}

// -----------------------------------------------------------------------------

const alphaRegex = /[a-zA-Z]/g;
const nonAlphaRegex = /[^a-zA-Z]/g;
const nonDigitsRegex = /[^0-9]/g;
// Parsing
// -----------------------------------------------------------------------------

/**
* Removes any duplicated alphabetic characters in a given string.
* Example: "aa-bb-CCcc-ddD xxxx-Yy-ZZ" -> "a-b-Cc-dD x-Yy-Z"
* Parses a string value to an UTC Luxon DateTime object.
*
* @param {string} str
* @returns {string}
*/
function stripAlphaDupes(str) {
return str.replace(alphaRegex, (letter, index, str) => {
return letter === str[index - 1] ? "" : letter;
});
}

/**
* Confirms a Luxon DateTime object.
* Returns it if within some defined properties.
* Returns false otherwise.
* @param {string} value value to parse.
* - Value can take the form of a smart date:
* e.g. "+3w" for three weeks from now.
* (`options.format` and `options.timezone` are ignored in this case)
*
* @param {DateTime | false} date
* @returns {DateTime | false}
* - If value cannot be parsed within the provided format,
* ISO8601 and SQL formats are then tried.
*
* @param {object} options
* @param {string} [options.format]
* Provided format used to parse the input value.
* Default=the session localization format
* @returns {DateTime|false} Luxon DateTime object (in the UTC timezone)
*/
function confirm(date) {
let valid = date !== false;
valid = valid && date.isValid;
valid = valid && date.year >= 1000;
valid = valid && date.year < 10000;
return valid ? date : false;
export function parseDate(value, options = {}) {
return parseDateTime(value, { dateOnly: true, format: options.format });
}

/**
Expand Down Expand Up @@ -185,7 +244,7 @@ export function parseDateTime(value, options = {}) {
zone: options.timezone ? "local" : "utc",
};

let result = confirm(parseSmartDateInput(value));
let result = constrain(parseSmartDateInput(value));

if (!result) {
const fmt = options.format || localization.dateTimeFormat;
Expand All @@ -200,9 +259,9 @@ export function parseDateTime(value, options = {}) {
};

result =
confirm(DateTime.fromFormat(value, fmt, parseOpts)) ||
confirm(DateTime.fromFormat(value, fmtWoZero, parseOpts)) ||
confirm(DateTime.fromFormat(woSeps.val, woSeps.fmt, parseOpts));
constrain(DateTime.fromFormat(value, fmt, parseOpts)) ||
constrain(DateTime.fromFormat(value, fmtWoZero, parseOpts)) ||
constrain(DateTime.fromFormat(woSeps.val, woSeps.fmt, parseOpts));
}

if (!result) {
Expand All @@ -211,8 +270,8 @@ export function parseDateTime(value, options = {}) {
// four digit characters as this could get misinterpreted as the time of
// the actual date.
result =
confirm(DateTime.fromISO(value, parseOpts)) || // ISO8601
confirm(DateTime.fromSQL(value, parseOpts)); // last try: SQL
constrain(DateTime.fromISO(value, parseOpts)) || // ISO8601
constrain(DateTime.fromSQL(value, parseOpts)); // last try: SQL
}
}

Expand All @@ -223,64 +282,3 @@ export function parseDateTime(value, options = {}) {
result = result.toUTC();
return options.dateOnly ? result.startOf("day") : result;
}

/**
* Parses a string value to an UTC Luxon DateTime object.
*
* @param {string} value value to parse.
* - Value can take the form of a smart date:
* e.g. "+3w" for three weeks from now.
* (`options.format` and `options.timezone` are ignored in this case)
*
* - If value cannot be parsed within the provided format,
* ISO8601 and SQL formats are then tried.
*
* @param {object} options
* @param {string} [options.format]
* Provided format used to parse the input value.
* Default=the session localization format
* @returns {DateTime|false} Luxon DateTime object (in the UTC timezone)
*/
export function parseDate(value, options = {}) {
return parseDateTime(value, { dateOnly: true, format: options.format });
}

// -----------------------------------------------------------------------------

const dateUnits = {
d: "days",
m: "months",
w: "weeks",
y: "years",
};

const smartDateRegex = new RegExp(`^([+-])(\\d+)([${Object.keys(dateUnits).join("")}]?)$`);

/**
* Smart date inputs are shortcuts to write dates quicker.
* These shortcuts should respect the format ^[+-]\d+[dmwy]?$
*
* e.g.
* "+1d" or "+1" will return now + 1 day
* "-2w" will return now - 2 weeks
* "+3m" will return now + 3 months
* "-4y" will return now + 4 years
*
* @param {string} value
* @returns {DateTime|false} Luxon datetime object (in the UTC timezone)
*/
export function parseSmartDateInput(value) {
const match = smartDateRegex.exec(value);
if (match) {
let date = DateTime.utc();
const offset = parseInt(match[2], 10);
const unit = dateUnits[match[3] || "d"];
if (match[1] === "+") {
date = date.plus({ [unit]: offset });
} else {
date = date.minus({ [unit]: offset });
}
return date;
}
return false;
}