[FIX] web: py.js datetime.timedelta implementation

The identification of timedelta with relativedelta was wrong.

Here we define a class PyTimeDelta used by py.js to evaluate most
expressions in which datetime.timedelta and some operations producing
timedeltas occur.

The class PyTimeDelta allows to:
    - create a timedelta
    - add/substract two timedeltas
    - multiply (divide) a timedelta by a float (resp. an integer);
    - take opposite of a timedelta
    - evaluate equalities between timedeltas and other expressions
    - decide when a timedelta is "false"
    - get total_seconds of a timedelta

In relation with that, we have improved the implementation of PyDate
and PyDateTime to allow most manipulations of datetime.date and
datetime.datetime producing/using timedeltas. It is now also possible to
evaluate equalities between dates/datetimes and other objects.

closes odoo/odoo#77453

X-original-commit: fcd4959a65a92e34ce986575c7fc279c195e9a11
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
Signed-off-by: Mathieu Duckerts-Antoine <Polymorphe57@users.noreply.github.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2021-09-30 10:38:19 +00:00
parent 01ffaea48b
commit da8f749f56
5 changed files with 642 additions and 46 deletions
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { PyDate, PyDateTime, PyRelativeDelta, PyTime } from "./py_date";
import { PyDate, PyDateTime, PyRelativeDelta, PyTime, PyTimeDelta } from "./py_date";
export const BUILTINS = {
/**
@@ -16,7 +16,13 @@ export const BUILTINS = {
case "boolean":
return value;
case "object":
return value !== null;
if (value === null) {
return false;
}
if (value.isTrue) {
return value.isTrue();
}
return true;
}
return true;
},
@@ -46,7 +52,7 @@ export const BUILTINS = {
datetime: {
time: PyTime,
timedelta: PyRelativeDelta,
timedelta: PyTimeDelta,
datetime: PyDateTime,
date: PyDate,
},
+275 -13
View File
@@ -231,6 +231,11 @@ export class PyDate {
return new PyDate(year, month, day);
}
/**
* @param {integer} year
* @param {integer} month
* @param {integer} day
*/
constructor(year, month, day) {
this.year = year;
this.month = month; // 1-indexed => 1 = january, 2 = february, ...
@@ -246,6 +251,26 @@ export class PyDate {
return new PyDate(year, month, day);
}
/**
* @param {PyTimeDelta} timedelta
* @returns {PyDate}
*/
add(timedelta) {
const s = tmxxx(this.year, this.month, this.day + timedelta.days);
return new PyDate(s.year, s.month, s.day);
}
/**
* @param {any} other
* @returns {boolean}
*/
isEqual(other) {
if (!(other instanceof PyDate)) {
return false;
}
return this.year === other.year && this.month === other.month && this.day === other.day;
}
/**
* @param {string} format
* @returns {string}
@@ -264,9 +289,33 @@ export class PyDate {
});
}
/**
* @param {PyTimeDelta | PyDate} other
* @returns {PyDate | PyTimeDelta}
*/
substract(other) {
if (other instanceof PyTimeDelta) {
return this.add(other.negate());
}
if (other instanceof PyDate) {
return PyTimeDelta.create(this.toordinal() - other.toordinal());
}
throw NotSupportedError();
}
/**
* @returns {string}
*/
toJSON() {
return this.strftime("%Y-%m-%d");
}
/**
* @returns {integer}
*/
toordinal() {
return ymd2ord(this.year, this.month, this.day);
}
}
export class PyDateTime {
@@ -325,14 +374,60 @@ export class PyDateTime {
);
}
constructor(year, month, day, hour, minute, second, millisecond) {
/**
* @param {integer} year
* @param {integer} month
* @param {integer} day
* @param {integer} hour
* @param {integer} minute
* @param {integer} second
* @param {integer} microsecond
*/
constructor(year, month, day, hour, minute, second, microsecond) {
this.year = year;
this.month = month; // 1-indexed => 1 = january, 2 = february, ...
this.day = day; // 1-indexed => 1 = first day of month, ...
this.hour = hour;
this.minute = minute;
this.second = second;
this.millisecond = millisecond;
this.microsecond = microsecond;
}
/**
* @param {PyTimeDelta} timedelta
* @returns {PyDate}
*/
add(timedelta) {
const s = tmxxx(
this.year,
this.month,
this.day + timedelta.days,
this.hour,
this.minute,
this.second + timedelta.seconds,
this.microsecond + timedelta.microseconds
);
// does not seem to closely follow python implementation.
return new PyDateTime(s.year, s.month, s.day, s.hour, s.minute, s.second, s.microsecond);
}
/**
* @param {any} other
* @returns {boolean}
*/
isEqual(other) {
if (!(other instanceof PyDateTime)) {
return false;
}
return (
this.year === other.year &&
this.month === other.month &&
this.day === other.day &&
this.hour === other.hour &&
this.minute === other.minute &&
this.second === other.second &&
this.microsecond === other.microsecond
);
}
/**
@@ -359,23 +454,28 @@ export class PyDateTime {
});
}
/**
* @param {PyTimeDelta} timedelta
* @returns {PyDateTime}
*/
substract(timedelta) {
return this.add(timedelta.negate());
}
/**
* @returns {string}
*/
toJSON() {
return this.strftime("%Y-%m-%d %H:%M:%S");
}
/**
* @returns {PyDateTime}
*/
to_utc() {
const d = new Date(this.year, this.month, this.day, this.hour, this.minute, this.second);
const offset = d.getTimezoneOffset();
// previous implementation did use timedelta
const s = tmxxx(
this.year,
this.month,
this.day,
this.hour,
this.minute,
this.second + 60 * offset
);
return new PyDateTime(s.year, s.month, s.day, s.hour, s.minute, s.second);
const timedelta = PyTimeDelta.create({ minutes: d.getTimezoneOffset() });
return this.add(timedelta);
}
}
@@ -500,3 +600,165 @@ export class PyRelativeDelta {
this.second = 0;
}
}
const TIME_DELTA_KEYS = "weeks days hours minutes seconds milliseconds microseconds".split(" ");
/**
* Returns a "pair" with the fractional and integer parts of x
* @param {float}
* @returns {[float,integer]}
*/
function modf(x) {
const mod = x % 1;
return [mod < 0 ? mod + 1 : mod, Math.floor(x)];
}
export class PyTimeDelta {
/**
* @param {...any} args
* @returns {PyTimeDelta}
*/
static create(...args) {
const namedArgs = parseArgs(args, ["days", "seconds", "microseconds"]);
for (const key of TIME_DELTA_KEYS) {
namedArgs[key] = namedArgs[key] || 0;
}
// a timedelta can be created using TIME_DELTA_KEYS with float/integer values
// but only days, seconds, microseconds are kept internally.
// --> some normalization occurs here
let d = 0;
let s = 0;
let us = 0; // ~ μs standard notation for microseconds
const days = namedArgs.days + namedArgs.weeks * 7;
let seconds = namedArgs.seconds + 60 * namedArgs.minutes + 3600 * namedArgs.hours;
let microseconds = namedArgs.microseconds + 1000 * namedArgs.milliseconds;
const [dFrac, dInt] = modf(days);
d = dInt;
let daysecondsfrac = 0;
if (dFrac) {
const [dsFrac, dsInt] = modf(dFrac * 24 * 3600);
s = dsInt;
daysecondsfrac = dsFrac;
}
const [sFrac, sInt] = modf(seconds);
seconds = sInt;
const secondsfrac = sFrac + daysecondsfrac;
divmod(seconds, 24 * 3600, (days, seconds) => {
d += days;
s += seconds;
});
microseconds += secondsfrac * 1e6;
divmod(microseconds, 1000000, (seconds, microseconds) => {
divmod(seconds, 24 * 3600, (days, seconds) => {
d += days;
s += seconds;
us += Math.round(microseconds);
});
});
return new PyTimeDelta(d, s, us);
}
/**
* @param {integer} days
* @param {integer} seconds
* @param {integer} microseconds
*/
constructor(days, seconds, microseconds) {
this.days = days;
this.seconds = seconds;
this.microseconds = microseconds;
}
/**
* @param {PyTimeDelta} other
* @returns {PyTimeDelta}
*/
add(other) {
return PyTimeDelta.create({
days: this.days + other.days,
seconds: this.seconds + other.seconds,
microseconds: this.microseconds + other.microseconds,
});
}
/**
* @param {integer} n
* @returns {PyTimeDelta}
*/
divide(n) {
const us = (this.days * 24 * 3600 + this.seconds) * 1e6 + this.microseconds;
return PyTimeDelta.create({ microseconds: Math.floor(us / n) });
}
/**
* @param {any} other
* @returns {boolean}
*/
isEqual(other) {
if (!(other instanceof PyTimeDelta)) {
return false;
}
return (
this.days === other.days &&
this.seconds === other.seconds &&
this.microseconds === other.microseconds
);
}
/**
* @returns {boolean}
*/
isTrue() {
return this.days !== 0 || this.seconds !== 0 || this.microseconds !== 0;
}
/**
* @param {float} n
* @returns {PyTimeDelta}
*/
multiply(n) {
return PyTimeDelta.create({
days: n * this.days,
seconds: n * this.seconds,
microseconds: n * this.microseconds,
});
}
/**
* @returns {PyTimeDelta}
*/
negate() {
return PyTimeDelta.create({
days: -this.days,
seconds: -this.seconds,
microseconds: -this.microseconds,
});
}
/**
* @param {PyTimeDelta} other
* @returns {PyTimeDelta}
*/
substract(other) {
return PyTimeDelta.create({
days: this.days - other.days,
seconds: this.seconds - other.seconds,
microseconds: this.microseconds - other.microseconds,
});
}
/**
* @returns {float}
*/
total_seconds() {
return this.days * 86400 + this.seconds + this.microseconds / 1000000;
}
}
@@ -1,7 +1,14 @@
/** @odoo-module **/
import { BUILTINS } from "./py_builtin";
import { PyDate, PyDateTime, PyRelativeDelta, PyTime } from "./py_date";
import {
NotSupportedError,
PyDate,
PyDateTime,
PyRelativeDelta,
PyTime,
PyTimeDelta,
} from "./py_date";
import { parseArgs, PY_DICT, toPyDict } from "./py_utils";
// -----------------------------------------------------------------------------
@@ -26,14 +33,17 @@ const isTrue = BUILTINS.bool;
* @returns {any}
*/
function applyUnaryOp(ast, context) {
const expr = evaluate(ast.right, context);
const value = evaluate(ast.right, context);
switch (ast.op) {
case "-":
return -expr;
if (value instanceof Object && value.negate) {
return value.negate();
}
return -value;
case "+":
return expr;
return value;
case "not":
return !isTrue(expr);
return !isTrue(value);
}
throw new EvaluationError(`Unknown unary operator: ${ast.op}`);
}
@@ -59,6 +69,14 @@ function pytypeIndex(val) {
throw new EvaluationError(`Unknown type: ${typeof val}`);
}
/**
* @param {Object} obj
* @returns {boolean}
*/
function isConstructor(obj) {
return !!obj.prototype && !!obj.prototype.constructor.name;
}
/**
* Compare two values
*
@@ -99,6 +117,9 @@ function isEqual(left, right) {
}
return false;
}
if (left instanceof Object && left.isEqual) {
return left.isEqual(right);
}
return left === right;
}
@@ -127,13 +148,34 @@ function applyBinaryOp(ast, context) {
const right = evaluate(ast.right, context);
switch (ast.op) {
case "+": {
const isLeftDelta = left instanceof PyRelativeDelta;
const isRightDelta = right instanceof PyRelativeDelta;
if (isLeftDelta || isRightDelta) {
const date = isLeftDelta ? right : left;
const delta = isLeftDelta ? left : right;
const relativeDeltaOnLeft = left instanceof PyRelativeDelta;
const relativeDeltaOnRight = right instanceof PyRelativeDelta;
if (relativeDeltaOnLeft || relativeDeltaOnRight) {
const date = relativeDeltaOnLeft ? right : left;
const delta = relativeDeltaOnLeft ? left : right;
return PyRelativeDelta.add(date, delta);
}
const timeDeltaOnLeft = left instanceof PyTimeDelta;
const timeDeltaOnRight = right instanceof PyTimeDelta;
if (timeDeltaOnLeft && timeDeltaOnRight) {
return left.add(right);
}
if (timeDeltaOnLeft) {
if (right instanceof PyDate || right instanceof PyDateTime) {
return right.add(left);
} else {
throw NotSupportedError();
}
}
if (timeDeltaOnRight) {
if (left instanceof PyDate || left instanceof PyDateTime) {
return left.add(right);
} else {
throw NotSupportedError();
}
}
return left + right;
}
case "-": {
@@ -141,15 +183,41 @@ function applyBinaryOp(ast, context) {
if (isRightDelta) {
return PyRelativeDelta.substract(left, right);
}
const timeDeltaOnRight = right instanceof PyTimeDelta;
if (timeDeltaOnRight) {
if (left instanceof PyTimeDelta) {
return left.substract(right);
} else if (left instanceof PyDate || left instanceof PyDateTime) {
return left.substract(right);
} else {
throw NotSupportedError();
}
}
if (left instanceof PyDate) {
return left.substract(right);
}
return left - right;
}
case "*":
const timeDeltaOnLeft = left instanceof PyTimeDelta;
const timeDeltaOnRight = right instanceof PyTimeDelta;
if (timeDeltaOnLeft || timeDeltaOnRight) {
const number = timeDeltaOnLeft ? right : left;
const delta = timeDeltaOnLeft ? left : right;
return delta.multiply(number); // check number type?
}
return left * right;
case "/":
return left / right;
case "%":
return left % right;
case "//":
if (left instanceof PyTimeDelta) {
return left.divide(right); // check number type?
}
return Math.floor(left / right);
case "**":
return left ** right;
@@ -262,7 +330,8 @@ export function evaluate(ast, context = {}) {
fnValue === PyDate ||
fnValue === PyDateTime ||
fnValue === PyTime ||
fnValue === PyRelativeDelta
fnValue === PyRelativeDelta ||
fnValue === PyTimeDelta
) {
return fnValue.create(...args, kwargs);
}
@@ -285,11 +354,11 @@ export function evaluate(ast, context = {}) {
// this is a dictionary => need to apply dict methods
return DICT[ast.key](left);
}
if (left instanceof PyDate || left instanceof PyDateTime) {
const result = left[ast.key];
return typeof result === "function" ? result.bind(left) : result;
const result = left[ast.key];
if (typeof result === "function" && !isConstructor(result)) {
return result.bind(left);
}
return left[ast.key];
return result;
}
}
throw new EvaluationError(`AST of type ${ast.type} cannot be evaluated`);
@@ -2,6 +2,7 @@
import { evaluateExpr } from "@web/core/py_js/py";
import { patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
import { PyDate, PyTimeDelta } from "@web/core/py_js/py_date";
QUnit.module("py", {}, () => {
QUnit.module("date stuff", () => {
@@ -87,7 +88,47 @@ QUnit.module("py", {}, () => {
);
});
// datetime.datetime.combine(context_today(), datetime.time(23,59,59))
QUnit.test("datetime + timedelta", function (assert) {
assert.expect(6);
assert.strictEqual(
evaluateExpr(
"(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"
),
"2017-02-16 01:07:31"
);
assert.strictEqual(
evaluateExpr(
"(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"
),
"2012-02-15 00:07:31"
);
assert.strictEqual(
evaluateExpr(
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')"
),
"2012-02-15 00:07:31"
);
assert.strictEqual(
evaluateExpr(
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')"
),
"2012-02-15 02:47:31"
);
assert.strictEqual(
evaluateExpr(
"(datetime.date(day=3,month=4,year=2001) + datetime.timedelta(days=-1)).strftime('%Y-%m-%d')"
),
"2001-04-02"
);
assert.strictEqual(
evaluateExpr(
"(datetime.timedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')"
),
"2001-04-02"
);
});
QUnit.module("datetime.date");
QUnit.test("datetime.date.today", (assert) => {
@@ -108,6 +149,84 @@ QUnit.module("py", {}, () => {
);
});
QUnit.test("basic operations with dates", function (assert) {
assert.expect(19);
let ctx = {
d1: PyDate.create(2002, 1, 31),
d2: PyDate.create(1956, 1, 31),
};
assert.strictEqual(evaluateExpr("(d1 - d2).days", ctx), 46 * 365 + 12);
assert.strictEqual(evaluateExpr("(d1 - d2).seconds", ctx), 0);
assert.strictEqual(evaluateExpr("(d1 - d2).microseconds", ctx), 0);
ctx = {
a: PyDate.create(2002, 3, 2),
day: PyTimeDelta.create({ days: 1 }),
week: PyTimeDelta.create({ days: 7 }),
date: PyDate,
};
assert.ok(evaluateExpr("a + day == date(2002, 3, 3)", ctx));
assert.ok(evaluateExpr("day + a == date(2002, 3, 3)", ctx)); // 5
assert.ok(evaluateExpr("a - day == date(2002, 3, 1)", ctx));
assert.ok(evaluateExpr("-day + a == date(2002, 3, 1)", ctx));
assert.ok(evaluateExpr("a + week == date(2002, 3, 9)", ctx));
assert.ok(evaluateExpr("a - week == date(2002, 2, 23)", ctx));
assert.ok(evaluateExpr("a + 52*week == date(2003, 3, 1)", ctx)); // 10
assert.ok(evaluateExpr("a - 52*week == date(2001, 3, 3)", ctx));
assert.ok(evaluateExpr("(a + week) - a == week", ctx));
assert.ok(evaluateExpr("(a + day) - a == day", ctx));
assert.ok(evaluateExpr("(a - week) - a == -week", ctx));
assert.ok(evaluateExpr("(a - day) - a == -day", ctx)); // 15
assert.ok(evaluateExpr("a - (a + week) == -week", ctx));
assert.ok(evaluateExpr("a - (a + day) == -day", ctx));
assert.ok(evaluateExpr("a - (a - week) == week", ctx));
assert.ok(evaluateExpr("a - (a - day) == day", ctx));
// assert.throws(function () {
// evaluateExpr("a + 1", ctx);
// }, /^Error: TypeError:/); //20
// assert.throws(function () {
// evaluateExpr("a - 1", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("1 + a", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("1 - a", ctx);
// }, /^Error: TypeError:/);
// // delta - date is senseless.
// assert.throws(function () {
// evaluateExpr("day - a", ctx);
// }, /^Error: TypeError:/);
// // mixing date and (delta or date) via * or // is senseless
// assert.throws(function () {
// evaluateExpr("day * a", ctx);
// }, /^Error: TypeError:/); // 25
// assert.throws(function () {
// evaluateExpr("a * day", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("day // a", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("a // day", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("a * a", ctx);
// }, /^Error: TypeError:/);
// assert.throws(function () {
// evaluateExpr("a // a", ctx);
// }, /^Error: TypeError:/); // 30
// // date + date is senseless
// assert.throws(function () {
// evaluateExpr("a + a", ctx);
// }, /^Error: TypeError:/);
});
QUnit.module("datetime.time");
QUnit.test("various operations", (assert) => {
@@ -163,20 +282,6 @@ QUnit.module("py", {}, () => {
assert.strictEqual(evaluateExpr(expr4), "2001-03-27");
});
QUnit.module("datetime.timedelta");
QUnit.test("adding date and time delta", (assert) => {
const expr =
"(datetime.date(day=3,month=4,year=2001) + datetime.timedelta(days=-1)).strftime('%Y-%m-%d')";
assert.strictEqual(evaluateExpr(expr), "2001-04-02");
});
QUnit.test("adding time delta and date", (assert) => {
const expr =
"(datetime.timedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
assert.strictEqual(evaluateExpr(expr), "2001-04-02");
});
QUnit.module("misc");
QUnit.test("context_today", (assert) => {
@@ -0,0 +1,154 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { PyTimeDelta } from "@web/core/py_js/py_date";
function testDelta(assert, expr, res) {
const timedelta = evaluateExpr(expr, { td: PyTimeDelta });
assert.strictEqual(`${timedelta.days}, ${timedelta.seconds}, ${timedelta.microseconds}`, res);
}
function testEquality(assert, expr1, expr2, ctx) {
const equality = `${expr1} == ${expr2}`;
assert.ok(
evaluateExpr(equality, Object.assign({ td: PyTimeDelta }, ctx)),
`evaluating ${equality}`
);
}
QUnit.module("py", {}, () => {
QUnit.module("datetime.timedelta");
QUnit.test("create", (assert) => {
testDelta(assert, "td(weeks=1)", "7, 0, 0");
testDelta(assert, "td(days=1)", "1, 0, 0");
testDelta(assert, "td(hours=1)", "0, 3600, 0");
testDelta(assert, "td(minutes=1)", "0, 60, 0");
testDelta(assert, "td(seconds=1)", "0, 1, 0");
testDelta(assert, "td(milliseconds=1)", "0, 0, 1000");
testDelta(assert, "td(microseconds=1)", "0, 0, 1");
testDelta(assert, "td(days=-1.25)", "-2, 64800, 0");
testDelta(assert, "td(seconds=129600.4)", "1, 43200, 400000");
testDelta(assert, "td(hours=24.5,milliseconds=1400)", "1, 1801, 400000");
testEquality(
assert,
"td()",
"td(weeks=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0)"
);
testEquality(assert, "td(1)", "td(days=1)");
testEquality(assert, "td(0, 1)", "td(seconds=1)");
testEquality(assert, "td(0, 0, 1)", "td(microseconds=1)");
testEquality(assert, "td(weeks=1)", "td(days=7)");
testEquality(assert, "td(days=1)", "td(hours=24)");
testEquality(assert, "td(hours=1)", "td(minutes=60)");
testEquality(assert, "td(minutes=1)", "td(seconds=60)");
testEquality(assert, "td(seconds=1)", "td(milliseconds=1000)");
testEquality(assert, "td(milliseconds=1)", "td(microseconds=1000)");
testEquality(assert, "td(weeks=1.0/7)", "td(days=1)");
testEquality(assert, "td(days=1.0/24)", "td(hours=1)");
testEquality(assert, "td(hours=1.0/60)", "td(minutes=1)");
testEquality(assert, "td(minutes=1.0/60)", "td(seconds=1)");
testEquality(assert, "td(seconds=0.001)", "td(milliseconds=1)");
testEquality(assert, "td(milliseconds=0.001)", "td(microseconds=1)");
});
QUnit.test("massive normalization", function (assert) {
assert.expect(3);
const td = PyTimeDelta.create({ microseconds: -1 });
assert.strictEqual(td.days, -1);
assert.strictEqual(td.seconds, 24 * 3600 - 1);
assert.strictEqual(td.microseconds, 999999);
});
QUnit.test("attributes", function (assert) {
assert.expect(3);
const ctx = { td: PyTimeDelta };
assert.strictEqual(evaluateExpr("td(1, 7, 31).days", ctx), 1);
assert.strictEqual(evaluateExpr("td(1, 7, 31).seconds", ctx), 7);
assert.strictEqual(evaluateExpr("td(1, 7, 31).microseconds", ctx), 31);
});
QUnit.test("basic operations: +, -, *, //", function (assert) {
assert.expect(28);
const ctx = {
a: new PyTimeDelta(7, 0, 0),
b: new PyTimeDelta(0, 60, 0),
c: new PyTimeDelta(0, 0, 1000),
};
testEquality(assert, "a+b+c", "td(7, 60, 1000)", ctx);
testEquality(assert, "a-b", "td(6, 24*3600 - 60)", ctx);
testEquality(assert, "-a", "td(-7)", ctx);
testEquality(assert, "+a", "td(7)", ctx);
testEquality(assert, "-b", "td(-1, 24*3600 - 60)", ctx);
testEquality(assert, "-c", "td(-1, 24*3600 - 1, 999000)", ctx);
testEquality(assert, "td(6, 24*3600)", "a", ctx);
testEquality(assert, "td(0, 0, 60*1000000)", "b", ctx);
testEquality(assert, "a*10", "td(70)", ctx);
testEquality(assert, "a*10", "10*a", ctx);
// testEquality(assert, 'a*10L', '10*a', ctx);
testEquality(assert, "b*10", "td(0, 600)", ctx);
testEquality(assert, "10*b", "td(0, 600)", ctx);
// testEquality(assert, 'b*10L', 'td(0, 600)', ctx);
testEquality(assert, "c*10", "td(0, 0, 10000)", ctx);
testEquality(assert, "10*c", "td(0, 0, 10000)", ctx);
// testEquality(assert, 'c*10L', 'td(0, 0, 10000)', ctx);
testEquality(assert, "a*-1", "-a", ctx);
testEquality(assert, "b*-2", "-b-b", ctx);
testEquality(assert, "c*-2", "-c+-c", ctx);
testEquality(assert, "b*(60*24)", "(b*60)*24", ctx);
testEquality(assert, "b*(60*24)", "(60*b)*24", ctx);
testEquality(assert, "c*1000", "td(0, 1)", ctx);
testEquality(assert, "1000*c", "td(0, 1)", ctx);
testEquality(assert, "a//7", "td(1)", ctx);
testEquality(assert, "b//10", "td(0, 6)", ctx);
testEquality(assert, "c//1000", "td(0, 0, 1)", ctx);
testEquality(assert, "a//10", "td(0, 7*24*360)", ctx);
testEquality(assert, "a//3600000", "td(0, 0, 7*24*1000)", ctx);
testEquality(
assert,
"td(999999999, 86399, 999999) - td(999999999, 86399, 999998)",
"td(0, 0, 1)"
);
testEquality(assert, "td(999999999, 1, 1) - td(999999999, 1, 0)", "td(0, 0, 1)");
});
QUnit.test("total_seconds", function (assert) {
assert.expect(6);
const ctx = { td: PyTimeDelta };
assert.strictEqual(evaluateExpr("td(365).total_seconds()", ctx), 31536000);
assert.strictEqual(
evaluateExpr("td(seconds=123456.789012).total_seconds()", ctx),
123456.789012
);
assert.strictEqual(
evaluateExpr("td(seconds=-123456.789012).total_seconds()", ctx),
-123456.789012
);
assert.strictEqual(evaluateExpr("td(seconds=0.123456).total_seconds()", ctx), 0.123456);
assert.strictEqual(evaluateExpr("td().total_seconds()", ctx), 0);
assert.strictEqual(evaluateExpr("td(seconds=1000000).total_seconds()", ctx), 1e6);
});
QUnit.test("bool", function (assert) {
assert.expect(5);
const ctx = { td: PyTimeDelta };
assert.ok(evaluateExpr("bool(td(1))", ctx));
assert.ok(evaluateExpr("bool(td(0, 1))", ctx));
assert.ok(evaluateExpr("bool(td(0, 0, 1))", ctx));
assert.ok(evaluateExpr("bool(td(microseconds=1))", ctx));
assert.ok(evaluateExpr("bool(not td(0))", ctx));
});
});