From 4f46ae4707495bb7afe3c9dc02fe165c5049dbcc Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 18 Oct 2024 18:17:15 +0100 Subject: [PATCH] New formatter function 'field_list_count('lookup_name')': returns the length of the multi-valued field 'lookup_name' without first converting it to a string. This is much faster than the existing function list_count() --- manual/template_lang.rst | 7 ++++--- src/calibre/utils/formatter.py | 25 +++++++++++++++++++++++- src/calibre/utils/formatter_functions.py | 19 +++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 22de357586..f29d086959 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -495,10 +495,11 @@ In `GPM` the functions described in `Single Function Mode` all require an additi * ``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 `. * ``extra_file_size(file_name)`` -- returns the size in bytes of the extra file ``file_name`` in the book's ``data/`` folder if it exists, otherwise ``-1``. See also the functions ``has_extra_files()``, ``extra_file_names()`` and ``extra_file_modtime()``. This function can be used only in the GUI. * ``extra_file_modtime(file_name, format_string)`` -- returns the modification time of the extra file ``file_name`` in the book's ``data/`` folder if it exists, otherwise ``-1``. The modtime is formatted according to ``format_string`` (see ``format_date()`` for details). If ``format_string`` is the empty string, returns the modtime as the floating point number of seconds since the epoch. See also the functions ``has_extra_files()``, ``extra_file_names()`` and ``extra_file_size()``. The epoch is OS dependent. This function can be used only in the GUI. -* ``extra_file_names(sep [, pattern])`` returns a ``sep``-separated list of extra files in the book's ``data/`` folder. If the optional parameter ``pattern``, a regular expression, is supplied then the list is filtered to files that match ``pattern``. The pattern match is case insensitive. See also the functions ``has_extra_files()``, ``extra_file_modtime()`` and ``extra_file_size()``. This function can be used only in the GUI. +* ``extra_file_names(sep [, pattern])`` -- returns a ``sep``-separated list of extra files in the book's ``data/`` folder. If the optional parameter ``pattern``, a regular expression, is supplied then the list is filtered to files that match ``pattern``. The pattern match is case insensitive. See also the functions ``has_extra_files()``, ``extra_file_modtime()`` and ``extra_file_size()``. This function can be used only in the GUI. * ``field(lookup_name)`` -- returns the value of the metadata field with lookup name ``lookup_name``. -* ``field_exists(field_name)`` -- checks if a field (column) with the lookup name ``field_name`` exists, returning ``'1'`` if so and the empty string if not. -* ``finish_formatting(val, fmt, prefix, suffix)`` -- apply the format, prefix, and suffix to a value in the same way as done in a template like ``{series_index:05.2f| - |- }``. This function is provided to ease conversion of complex single-function- or template-program-mode templates to `GPM` Templates. For example, the following program produces the same output as the above template:: +* ``field_exists(lookup_name)`` -- checks if a field (column) with the lookup name ``lookup_name`` exists, returning ``'1'`` if so and the empty string if not. +* ``field_list_count(lookup_name)``-- returns the count of items in the field with the lookup name `lookup_name`. The field must be multi-valued such as ``authors`` or ``tags``, otherwise the function raises an error. This function is much faster than ``list_count()`` because it operates directly on calibre data without converting it to a string first. Example: ``field_list_count('tags')`` +* ``finish_formatting(val, fmt, prefix, suffix)`` -- apply the format, prefix, and uffix to a value in the same way as done in a template like ``{series_index:05.2f| - |- }``. This function is provided to ease conversion of complex single-function- or template-program-mode templates to `GPM` Templates. For example, the following program produces the same output as the above template:: program: finish_formatting(field("series_index"), "05.2f", " - ", " - ") diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 8f09febd84..3049e64cb3 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -59,6 +59,7 @@ class Node: NODE_RANGE = 30 NODE_SWITCH = 31 NODE_SWITCH_IF = 32 + NODE_FIELD_LIST_COUNT = 33 def __init__(self, line_number, name): self.my_line_number = line_number @@ -331,6 +332,13 @@ class StrcatNode(Node): self.expression_list = expression_list +class FieldListCountNode(Node): + def __init__(self, line_number, expression): + Node.__init__(self, line_number, 'field_list_count()') + self.node_type = self.NODE_FIELD_LIST_COUNT + self.expression = expression + + class _Parser: LEX_OP = 1 LEX_ID = 2 @@ -709,7 +717,9 @@ class _Parser: 'print': (lambda _: True, lambda ln, args: PrintNode(ln, args)), 'strcat': (lambda _: True, - lambda ln, args: StrcatNode(ln, args)) + lambda ln, args: StrcatNode(ln, args)), + 'field_list_count': (lambda args: len(args) == 1, + lambda ln, args: FieldListCountNode(ln, args[0])) } def expr(self): @@ -1337,6 +1347,18 @@ class _Interpreter: self.break_reporter(prog.node_name, res, prog.line_number) return res + def do_node_field_list_count(self, prog): + name = field_metadata.search_term_to_field_key(self.expr(prog.expression)) + if not self.parent_book.has_key(name): + self.error(_("'{0}' is not a field").format(name), prog.line_number) + res = getattr(self.parent_book, name, None) + if not isinstance(res, (list, tuple, set, dict)): + self.error(_("Field '{0}' is not a list").format(name), prog.line_number) + ans = str(len(res)) + if self.break_reporter: + self.break_reporter(prog.node_name, ans, prog.line_number) + return ans + def do_node_break(self, prog): if (self.break_reporter): self.break_reporter(prog.node_name, '', prog.line_number) @@ -1566,6 +1588,7 @@ class _Interpreter: Node.NODE_BINARY_STRINGOP: do_node_stringops, Node.NODE_LOCAL_FUNCTION_DEFINE: do_node_local_function_define, Node.NODE_LOCAL_FUNCTION_CALL: do_node_local_function_call, + Node.NODE_FIELD_LIST_COUNT: do_node_field_list_count, } def expr(self, prog): diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index f786273bcf..b7045aa46d 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -2662,6 +2662,23 @@ class BuiltinIsDarkMode(BuiltinFormatterFunction): only_in_gui_error('is_dark_mode') +class BuiltinFieldListCount(BuiltinFormatterFunction): + name = 'field_list_count' + arg_count = 0 + category = 'List manipulation' + __doc__ = doc = _("field_list_count(field_name) -- returns the count of items " + "in the field with the lookup name 'field_name'. The field " + "must be multi-valued such as authors or tags, otherwise " + "the function raises an error. This function is much faster " + "than list_count() because it operates directly on calibre " + "data without converting it to a string first. " + "Example: {}").format("field_list_count('tags')") + + def evaluate(self, formatter, kwargs, mi, locals, *args): + # The globals function is implemented in-line in the formatter + raise NotImplementedError() + + _formatter_builtins = [ BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(), BuiltinAssign(), @@ -2673,7 +2690,7 @@ _formatter_builtins = [ BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(), - BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), + BuiltinFieldListCount(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(), BuiltinFormatDate(), BuiltinFormatDateField(), BuiltinFormatNumber(), BuiltinFormatsModtimes(), BuiltinFormatsPaths(), BuiltinFormatsSizes(), BuiltinFractionalPart(),