diff --git a/addons/web/static/src/core/py_js/py_builtin.js b/addons/web/static/src/core/py_js/py_builtin.js index a4d82730056..9a16bcd57bc 100644 --- a/addons/web/static/src/core/py_js/py_builtin.js +++ b/addons/web/static/src/core/py_js/py_builtin.js @@ -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, }, diff --git a/addons/web/static/src/core/py_js/py_date.js b/addons/web/static/src/core/py_js/py_date.js index 3259e171c77..aa7b5e30eec 100644 --- a/addons/web/static/src/core/py_js/py_date.js +++ b/addons/web/static/src/core/py_js/py_date.js @@ -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; + } +} diff --git a/addons/web/static/src/core/py_js/py_interpreter.js b/addons/web/static/src/core/py_js/py_interpreter.js index d2cac83b4f1..7be649c56fa 100644 --- a/addons/web/static/src/core/py_js/py_interpreter.js +++ b/addons/web/static/src/core/py_js/py_interpreter.js @@ -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`); diff --git a/addons/web/static/tests/core/py_js/py_date_tests.js b/addons/web/static/tests/core/py_js/py_date_tests.js index 9ed9ab4dc01..b40f16d8830 100644 --- a/addons/web/static/tests/core/py_js/py_date_tests.js +++ b/addons/web/static/tests/core/py_js/py_date_tests.js @@ -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) => { diff --git a/addons/web/static/tests/core/py_js/py_timedelta_tests.js b/addons/web/static/tests/core/py_js/py_timedelta_tests.js new file mode 100644 index 00000000000..84372966486 --- /dev/null +++ b/addons/web/static/tests/core/py_js/py_timedelta_tests.js @@ -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)); + }); +});