From 815b908759451efd2d74cdb8d16a46bb83fe25c0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 23 Nov 2012 08:59:38 +0100 Subject: [PATCH] [ADD] partial but sufficient (I think) implementation of datetime.timedelta bzr revid: xmo@openerp.com-20121123075938-oazwt4bn2l639r3a --- addons/web/static/src/js/pyeval.js | 187 ++++++++++++++++++++++++++++- addons/web/static/test/evals.js | 149 ++++++++++++++++++++++- 2 files changed, 332 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index fe68d5652a5..e0f465279ef 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -14,6 +14,188 @@ openerp.web.pyeval = function (instance) { }; var datetime = py.PY_call(py.object); + + /** + * computes (Math.floor(a/b), a%b and passes that to the callback. + * + * returns the callback's result + */ + var divmod = function (a, b, fn) { + var mod = a%b; + // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a + // round of b + if (mod > 0 && b < 0 || mod < 0 && b > 0) { + mod += b; + } + return fn(Math.floor(a/b), mod); + }; + /** + * Passes the fractional and integer parts of x to the callback, returns + * the callback's result + */ + var modf = function (x, fn) { + var mod = x%1; + if (mod < 0) { + mod += 1; + } + return fn(mod, Math.floor(x)); + }; + var zero = py.float.fromJSON(0); + // Port from pypy/lib_pypy/datetime.py + datetime.timedelta = py.type('timedelta', null, { + __init__: function () { + var args = py.PY_parseArgs(arguments, [ + ['days', zero], ['seconds', zero], ['microseconds', zero], + ['milliseconds', zero], ['minutes', zero], ['hours', zero], + ['weeks', zero] + ]); + + var d = 0, s = 0, m = 0; + var days = args.days.toJSON() + args.weeks.toJSON() * 7; + var seconds = args.seconds.toJSON() + + args.minutes.toJSON() * 60 + + args.hours.toJSON() * 3600; + var microseconds = args.microseconds.toJSON() + + args.milliseconds.toJSON() * 1000; + + // Get rid of all fractions, and normalize s and us. + // Take a deep breath . + var daysecondsfrac = modf(days, function (dayfrac, days) { + d = days; + if (dayfrac) { + return modf(dayfrac * 24 * 3600, function (dsf, dsw) { + s = dsw; + return dsf; + }); + } + return 0; + }); + + var secondsfrac = modf(seconds, function (sf, s) { + seconds = s; + return sf + daysecondsfrac; + }); + divmod(seconds, 24*3600, function (days, seconds) { + d += days; + s += seconds + }); + // seconds isn't referenced again before redefinition + + microseconds += secondsfrac * 1e6; + divmod(microseconds, 1000000, function (seconds, microseconds) { + divmod(seconds, 24*3600, function (days, seconds) { + d += days; + s += seconds; + m += Math.round(microseconds); + }); + }); + + // Carrying still possible here? + + this.days = d; + this.seconds = s; + this.microseconds = m; + }, + __str__: function () { + var hh, mm, ss; + divmod(this.seconds, 60, function (m, s) { + divmod(m, 60, function (h, m) { + hh = h; + mm = m; + ss = s; + }); + }); + var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss); + if (this.days) { + s = _.str.sprintf("%d day%s, %s", + this.days, + (this.days != 1 && this.days != -1) ? 's' : '', + s); + } + if (this.microseconds) { + s = _.str.sprintf("%s.%06d", s, this.microseconds); + } + return py.str.fromJSON(s); + }, + __eq__: function (other) { + if (py.PY_call(py.isinstance, [other, datetime.timedelta]) !== py.True) { + return py.False; + } + return (this.days === other.days + && this.seconds === other.seconds + && this.microseconds === other.microseconds) + ? py.True : py.False; + }, + __add__: function (other) { + if (py.PY_call(py.isinstance, [other, datetime.timedelta]) !== py.True) { + return py.NotImplemented; + } + return py.PY_call(datetime.timedelta, [ + py.float.fromJSON(this.days + other.days), + py.float.fromJSON(this.seconds + other.seconds), + py.float.fromJSON(this.microseconds + other.microseconds) + ]); + }, + __radd__: function (other) { return this.__add__(other); }, + __sub__: function (other) { + if (py.PY_call(py.isinstance, [other, datetime.timedelta]) !== py.True) { + return py.NotImplemented; + } + return py.PY_call(datetime.timedelta, [ + py.float.fromJSON(this.days - other.days), + py.float.fromJSON(this.seconds - other.seconds), + py.float.fromJSON(this.microseconds - other.microseconds) + ]); + }, + __rsub__: function (other) { + if (py.PY_call(py.isinstance, [other, datetime.timedelta]) !== py.True) { + return py.NotImplemented; + } + return this.__neg__().__add__(other); + }, + __neg__: function () { + return py.PY_call(datetime.timedelta, [ + py.float.fromJSON(-this.days), + py.float.fromJSON(-this.seconds), + py.float.fromJSON(-this.microseconds) + ]); + }, + __pos__: function () { return this; }, + __mul__: function (other) { + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + var n = other.toJSON(); + return py.PY_call(datetime.timedelta, [ + py.float.fromJSON(this.days * n), + py.float.fromJSON(this.seconds * n), + py.float.fromJSON(this.microseconds * n) + ]); + }, + __rmul__: function (other) { return this.__mul__(other); }, + __div__: function (other) { + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000 + + this.microseconds; + return py.PY_call( + datetime.timedelta, [ + zero, zero, py.float.fromJSON(usec / other.toJSON())]); + }, + __floordiv__: function (other) { return this.__div__(other); }, + total_seconds: function () { + return py.float.fromJSON( + this.days * 86400 + + this.seconds + + this.microseconds / 1000000) + }, + __nonzero__: function () { + return (!!this.days || !!this.seconds || !!this.microseconds) + ? py.True + : py.False; + } + }); datetime.datetime = py.type('datetime', null, { __init__: function () { var zero = py.float.fromJSON(0); @@ -108,7 +290,6 @@ openerp.web.pyeval = function (instance) { } } }); - var time = py.PY_call(py.object); time.strftime = py.PY_def.fromJSON(function () { // FIXME: needs PY_getattr @@ -127,7 +308,7 @@ openerp.web.pyeval = function (instance) { + 'weekday leakdays yearday nlyearday'); }, __add__: function (other) { - if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) { + if (py.PY_call(py.isinstance, [other, datetime.date]) !== py.True) { return py.NotImplemented; } // TODO: test this whole mess @@ -172,7 +353,7 @@ openerp.web.pyeval = function (instance) { }, __sub__: function (other) { - if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) { + if (py.PY_call(py.isinstance, [other, datetime.date]) !== py.True) { return py.NotImplemented; } // TODO: test this whole mess diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 2fe3168a357..ba9888617ee 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -1,5 +1,8 @@ openerp.testing.section('eval.types', { - dependencies: ['web.coresetup'] + dependencies: ['web.coresetup'], + setup: function (instance) { + instance.session.uid = 42; + } }, function (test) { test('strftime', function (instance) { var d = new Date(); @@ -16,6 +19,150 @@ openerp.testing.section('eval.types', { d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds())); }); + // Port from pypy/lib_pypy/test_datetime.py + var makeEq = function (instance, c2) { + var ctx = instance.web.pyeval.context(); + var c = _.extend({ td: ctx.datetime.timedelta }, c2 || {}); + return function (a, b, message) { + ok(py.eval(a + ' == ' + b, c), message); + }; + }; + test('timedelta.test_constructor', function (instance) { + var eq = makeEq(instance); + + // keyword args to constructor + eq('td()', 'td(weeks=0, days=0, hours=0, minutes=0, seconds=0, ' + + 'milliseconds=0, microseconds=0)'); + eq('td(1)', 'td(days=1)'); + eq('td(0, 1)', 'td(seconds=1)'); + eq('td(0, 0, 1)', 'td(microseconds=1)'); + eq('td(weeks=1)', 'td(days=7)'); + eq('td(days=1)', 'td(hours=24)'); + eq('td(hours=1)', 'td(minutes=60)'); + eq('td(minutes=1)', 'td(seconds=60)'); + eq('td(seconds=1)', 'td(milliseconds=1000)'); + eq('td(milliseconds=1)', 'td(microseconds=1000)'); + + // Check float args to constructor + eq('td(weeks=1.0/7)', 'td(days=1)'); + eq('td(days=1.0/24)', 'td(hours=1)'); + eq('td(hours=1.0/60)', 'td(minutes=1)'); + eq('td(minutes=1.0/60)', 'td(seconds=1)'); + eq('td(seconds=0.001)', 'td(milliseconds=1)'); + eq('td(milliseconds=0.001)', 'td(microseconds=1)'); + }); + test('timedelta.test_computations', function (instance) { + var c = instance.web.pyeval.context(); + var zero = py.float.fromJSON(0); + var eq = makeEq(instance, { + // one week + a: py.PY_call(c.datetime.timedelta, [ + py.float.fromJSON(7)]), + // one minute + b: py.PY_call(c.datetime.timedelta, [ + zero, py.float.fromJSON(60)]), + // one millisecond + c: py.PY_call(c.datetime.timedelta, [ + zero, zero, py.float.fromJSON(1000)]), + }); + + eq('a+b+c', 'td(7, 60, 1000)'); + eq('a-b', 'td(6, 24*3600 - 60)'); + eq('-a', 'td(-7)'); + eq('+a', 'td(7)'); + eq('-b', 'td(-1, 24*3600 - 60)'); + eq('-c', 'td(-1, 24*3600 - 1, 999000)'); +// eq('abs(a)', 'a'); +// eq('abs(-a)', 'a'); + eq('td(6, 24*3600)', 'a'); + eq('td(0, 0, 60*1000000)', 'b'); + eq('a*10', 'td(70)'); + eq('a*10', '10*a'); + // eq('a*10L', '10*a'); + eq('b*10', 'td(0, 600)'); + eq('10*b', 'td(0, 600)'); + // eq('b*10L', 'td(0, 600)'); + eq('c*10', 'td(0, 0, 10000)'); + eq('10*c', 'td(0, 0, 10000)'); + // eq('c*10L', 'td(0, 0, 10000)'); + eq('a*-1', '-a'); + eq('b*-2', '-b-b'); + eq('c*-2', '-c+-c'); + eq('b*(60*24)', '(b*60)*24'); + eq('b*(60*24)', '(60*b)*24'); + eq('c*1000', 'td(0, 1)'); + eq('1000*c', 'td(0, 1)'); + eq('a//7', 'td(1)'); + eq('b//10', 'td(0, 6)'); + eq('c//1000', 'td(0, 0, 1)'); + eq('a//10', 'td(0, 7*24*360)'); + eq('a//3600000', 'td(0, 0, 7*24*1000)'); + + // Issue #11576 + eq('td(999999999, 86399, 999999) - td(999999999, 86399, 999998)', 'td(0, 0, 1)'); + eq('td(999999999, 1, 1) - td(999999999, 1, 0)', + 'td(0, 0, 1)') + }); + test('timedelta.test_basic_attributes', function (instance) { + var ctx = instance.web.pyeval.context(); + strictEqual(py.eval('datetime.timedelta(1, 7, 31).days', ctx), 1); + strictEqual(py.eval('datetime.timedelta(1, 7, 31).seconds', ctx), 7); + strictEqual(py.eval('datetime.timedelta(1, 7, 31).microseconds', ctx), 31); + }); + test('timedelta.test_total_seconds', function (instance) { + var c = { timedelta: instance.web.pyeval.context().datetime.timedelta }; + strictEqual(py.eval('timedelta(365).total_seconds()', c), 31536000); + strictEqual( + py.eval('timedelta(seconds=123456.789012).total_seconds()', c), + 123456.789012); + strictEqual( + py.eval('timedelta(seconds=-123456.789012).total_seconds()', c), + -123456.789012); + strictEqual( + py.eval('timedelta(seconds=0.123456).total_seconds()', c), 0.123456); + strictEqual(py.eval('timedelta().total_seconds()', c), 0); + strictEqual( + py.eval('timedelta(seconds=1000000).total_seconds()', c), 1e6); + }); + test('timedelta.test_str', function (instance) { + var c = { td: instance.web.pyeval.context().datetime.timedelta }; + + strictEqual(py.eval('str(td(1))', c), "1 day, 0:00:00"); + strictEqual(py.eval('str(td(-1))', c), "-1 day, 0:00:00"); + strictEqual(py.eval('str(td(2))', c), "2 days, 0:00:00"); + strictEqual(py.eval('str(td(-2))', c), "-2 days, 0:00:00"); + + strictEqual(py.eval('str(td(hours=12, minutes=58, seconds=59))', c), + "12:58:59"); + strictEqual(py.eval('str(td(hours=2, minutes=3, seconds=4))', c), + "2:03:04"); + strictEqual( + py.eval('str(td(weeks=-30, hours=23, minutes=12, seconds=34))', c), + "-210 days, 23:12:34"); + + strictEqual(py.eval('str(td(milliseconds=1))', c), "0:00:00.001000"); + strictEqual(py.eval('str(td(microseconds=3))', c), "0:00:00.000003"); + + strictEqual( + py.eval('str(td(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999))', c), + "999999999 days, 23:59:59.999999"); + }); + test('timedelta.test_massive_normalization', function (instance) { + var td = py.PY_call( + instance.web.pyeval.context().datetime.timedelta, + {microseconds: py.float.fromJSON(-1)}); + strictEqual(td.days, -1); + strictEqual(td.seconds, 24 * 3600 - 1); + strictEqual(td.microseconds, 999999); + }); + test('timedelta.test_bool', function (instance) { + var c = { td: instance.web.pyeval.context().datetime.timedelta }; + ok(py.eval('bool(td(1))', c)); + ok(py.eval('bool(td(0, 1))', c)); + ok(py.eval('bool(td(0, 0, 1))', c)); + ok(py.eval('bool(td(microseconds=1))', c)); + ok(py.eval('bool(not td(0))', c)); + }); }); openerp.testing.section('eval.edc', { dependencies: ['web.data'],