From e03405df1b2ef5e010217dbe61019e161f44e7a8 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 3 Jun 2021 15:31:55 +0100 Subject: [PATCH] New formatter function 'date_arithmetic' that simplifies computing new dates from an existing one --- manual/template_lang.rst | 10 +++++ src/calibre/utils/formatter_functions.py | 52 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index cd407fab2a..db6272a2ed 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -389,6 +389,16 @@ In `GPM` the functions described in `Single Function Mode` all require an additi * ``connected_device_uuid(storage_location_key)`` -- if a device is connected then return the device uuid (unique id), otherwise return the empty string. Each storage location on a device has a different uuid. The ``storage_location_key`` location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI. * ``current_library_name()`` -- return the last name on the path to the current calibre library. * ``current_library_path()`` -- return the full path to the current calibre library. +* ``date_arithmetic(date, calc_spec, fmt)`` -- Calculate a new date from ``date`` using ``calc_spec``. Return the new date formatted according to optional ``fmt``: if not supplied then the result will be in ISO format. The calc_spec is a string formed by concatenating pairs of ``vW`` (``valueWhat``) where ``v`` is a possibly-negative number and W is one of the following letters: + + * ``s``: add ``v`` seconds to ``date`` + * ``m``: add ``v`` minutes to ``date`` + * ``h``: add ``v`` hours to ``date`` + * ``d``: add ``v`` days to ``date`` + * ``w``: add ``v`` weeks to ``date`` + * ``y``: add ``v`` years to ``date``, where a year is 365 days. + + Example: ``'1s3d-1m'`` will add 1 second, add 3 days, and subtract 1 month from ``date``. * ``days_between(date1, date2)`` -- return the number of days between ``date1`` and ``date2``. The number is positive if ``date1`` is greater than ``date2``, otherwise negative. If either ``date1`` or ``date2`` are not dates, the function returns the empty string. * ``divide(x, y)`` -- returns ``x / y``. Throws an exception if either ``x`` or ``y`` are not numbers. This function can usually be replaced by the ``/`` operator. * ``eval(string)`` -- evaluates the string as a program, passing the local variables. This permits using the template processor to construct complex results from local variables. In :ref:`Template Program Mode `, because the `{` and `}` characters are interpreted before the template is evaluated you must use `[[` for the `{` character and `]]` for the ``}`` character. They are converted automatically. Note also that prefixes and suffixes (the `|prefix|suffix` syntax) cannot be used in the argument to this function when using :ref:`Template Program Mode `. diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 3264fbfb7c..489ef41504 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -13,7 +13,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import inspect, re, traceback, numbers -from datetime import datetime +from datetime import datetime, timedelta from math import trunc, floor, ceil, modf from calibre import human_readable, prints @@ -1612,6 +1612,55 @@ class BuiltinDaysBetween(BuiltinFormatterFunction): return '%.1f'%(i.days + (i.seconds/(24.0*60.0*60.0))) +class BuiltinDateArithmetic(BuiltinFormatterFunction): + name = 'date_arithmetic' + arg_count = -1 + category = 'Date functions' + __doc__ = doc = _('date_arithmetic(date, calc_spec, fmt) -- ' + "Calculate a new date from 'date' using 'calc_spec'. Return the " + "new date formatted according to optional 'fmt': if not supplied " + "then the result will be in iso format. The calc_spec is a string " + "formed by concatenating pairs of 'vW' (valueWhat) where 'v' is a " + "possibly-negative number and W is one of the following letters: " + "s: add 'v' seconds to 'date' " + "m: add 'v' minutes to 'date' " + "h: add 'v' hours to 'date' " + "d: add 'v' days to 'date' " + "w: add 'v' weeks to 'date' " + "y: add 'v' years to 'date', where a year is 365 days. " + "Example: '1s3d-1m' will add 1 second, add 3 days, and subtract 1 " + "month from 'date'.") + + calc_ops = { + 's': lambda v: timedelta(seconds=v), + 'm': lambda v: timedelta(minutes=v), + 'h': lambda v: timedelta(hours=v), + 'd': lambda v: timedelta(days=v), + 'w': lambda v: timedelta(weeks=v), + 'y': lambda v: timedelta(days=v * 365), + } + + def evaluate(self, formatter, kwargs, mi, locals, date, calc_spec, fmt=None): + try: + d = parse_date(date) + if d == UNDEFINED_DATE: + return '' + while calc_spec: + mo = re.match('([-+\d]+)([smhdwy])', calc_spec) + if mo is None: + raise ValueError( + _("{0}: invalid calculation specifier '{1}'").format( + 'date_arithmetic', calc_spec)) + d += self.calc_ops[mo[2]](int(mo[1])) + calc_spec = calc_spec[len(mo[0]):] + return format_date(d, fmt if fmt else 'iso') + except ValueError as e: + raise e + except Exception as e: + traceback.print_exc() + raise ValueError(_("{0}: error: {1}").format('date_arithmetic', str(e))) + + class BuiltinLanguageStrings(BuiltinFormatterFunction): name = 'language_strings' arg_count = 2 @@ -2044,6 +2093,7 @@ _formatter_builtins = [ BuiltinCapitalize(), BuiltinCharacter(), BuiltinCheckYesNo(), BuiltinCeiling(), BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), + BuiltinDateArithmetic(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(),