From fb9a388c08405b9625a1dbd7f76324dd08df04be Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Mon, 14 Sep 2020 00:59:38 +0100 Subject: [PATCH] Unreported bug: the V4 template language treated None values as zero. These changes make the new processor do the same thing. Also update the documentation, --- manual/template_lang.rst | 2 +- src/calibre/utils/formatter.py | 136 +++++++++++++++++++++------------ 2 files changed, 89 insertions(+), 49 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 36da960f6a..89a6c3dce4 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -403,7 +403,7 @@ Program mode also supports the classic relational (comparison) operators: ``==`` * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns 'bar' if the book's series is not 'foo', else 'mumble'. * ``program: if or(field('series') == 'foo', field('series') == '1632') then 'yes' else 'no' fi`` returns 'yes' if series is either 'foo' or '1632', otherwise 'no'. * ``program: if '11' > '2' then 'yes' else 'no' fi`` returns 'no' because it is doing a lexical comparison. -If you want numeric comparison, use the operators ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. These operators return '' if either the left or the right side are the empty string, '1' if the operator evaluates to True, otherwise ''. +If you want numeric comparison instead of lexical comparison, use the operators ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. In this case the left and right values are set to zero if they are undefined or the empty string. If they are not numbers then an error is raised. The following example is a `program:` mode implementation of a recipe on the MobileRead forum: "Put series into the title, using either initials or a shortened form. Strip leading articles from the series name (any)." For example, for the book The Two Towers in the Lord of the Rings series, the recipe gives `LotR [02] The Two Towers`. Using standard templates, the recipe requires three custom columns and a plugboard, as explained in the following: diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index f6d30128d7..87ed0f18c4 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -23,10 +23,11 @@ class Node(object): NODE_IF = 2 NODE_ASSIGN = 3 NODE_FUNC = 4 - NODE_INFIX = 5 - NODE_CONSTANT = 6 - NODE_FIELD = 7 - NODE_RAW_FIELD = 8 + NODE_STRING_INFIX = 5 + NODE_NUMERIC_INFIX = 6 + NODE_CONSTANT = 7 + NODE_FIELD = 8 + NODE_RAW_FIELD = 9 class IfNode(Node): @@ -54,10 +55,19 @@ class FunctionNode(Node): self.expression_list = expression_list -class InfixNode(Node): +class StringInfixNode(Node): def __init__(self, operator, left, right): Node.__init__(self) - self.node_type = self.NODE_INFIX + self.node_type = self.NODE_STRING_INFIX + self.operator = operator + self.left = left + self.right = right + + +class NumericInfixNode(Node): + def __init__(self, operator, left, right): + Node.__init__(self) + self.node_type = self.NODE_NUMERIC_INFIX self.operator = operator self.left = left self.right = right @@ -92,12 +102,13 @@ class RawFieldNode(Node): class _Parser(object): - LEX_OP = 1 - LEX_ID = 2 - LEX_CONST = 3 - LEX_EOF = 4 - LEX_INFIX = 5 - LEX_KEYWORD = 6 + LEX_OP = 1 + LEX_ID = 2 + LEX_CONST = 3 + LEX_EOF = 4 + LEX_STRING_INFIX = 5 + LEX_NUMERIC_INFIX = 6 + LEX_KEYWORD = 7 def error(self, message): try: @@ -131,9 +142,15 @@ class _Parser(object): except: return False - def token_op_is_infix_compare(self): + def token_op_is_string_infix_compare(self): try: - return self.prog[self.lex_pos][0] == self.LEX_INFIX + return self.prog[self.lex_pos][0] == self.LEX_STRING_INFIX + except: + return False + + def token_op_is_numeric_infix_compare(self): + try: + return self.prog[self.lex_pos][0] == self.LEX_NUMERIC_INFIX except: return False @@ -251,10 +268,13 @@ class _Parser(object): def infix_expr(self): left = self.expr() - if not self.token_op_is_infix_compare(): - return left - operator = self.token() - return InfixNode(operator, left, self.expr()) + if self.token_op_is_string_infix_compare(): + operator = self.token() + return StringInfixNode(operator, left, self.expr()) + if self.token_op_is_numeric_infix_compare(): + operator = self.token() + return NumericInfixNode(operator, left, self.expr()) + return left def expr(self): if self.token_is_if(): @@ -324,25 +344,44 @@ class _Interpreter(object): val = self.expr(p) return val - INFIX_OPS = { + INFIX_STRING_OPS = { "==": lambda x, y: strcmp(x, y) == 0, "!=": lambda x, y: strcmp(x, y) != 0, "<": lambda x, y: strcmp(x, y) < 0, "<=": lambda x, y: strcmp(x, y) <= 0, ">": lambda x, y: strcmp(x, y) > 0, ">=": lambda x, y: strcmp(x, y) >= 0, - "==#": lambda x, y: float(x) == float(y) if x and y else False, - "!=#": lambda x, y: float(x) != float(y) if x and y else False, - "<#": lambda x, y: float(x) < float(y) if x and y else False, - "<=#": lambda x, y: float(x) <= float(y) if x and y else False, - ">#": lambda x, y: float(x) > float(y) if x and y else False, - ">=#": lambda x, y: float(x) >= float(y) if x and y else False, } - def do_node_infix(self, prog): - left = self.expr(prog.left) - right = self.expr(prog.right) - return '1' if self.INFIX_OPS[prog.operator](left, right) else '' + def do_node_string_infix(self, prog): + try: + left = self.expr(prog.left) + right = self.expr(prog.right) + return ('1' if self.INFIX_STRING_OPS[prog.operator](left, right) else '') + except: + self.error(_('Error during string comparison. Operator {0}').format(prog.operator)) + + INFIX_NUMERIC_OPS = { + "==#": lambda x, y: x == y, + "!=#": lambda x, y: x != y, + "<#": lambda x, y: x < y, + "<=#": lambda x, y: x <= y, + ">#": lambda x, y: x > y, + ">=#": lambda x, y: x >= y, + } + + def float_deal_with_none(self, v): + # Undefined values and the string 'None' are assumed to be zero. + # The reason for string 'None': raw_field returns it for undefined values + return float(v if v and v != 'None' else 0) + + def do_node_numeric_infix(self, prog): + try: + left = self.float_deal_with_none(self.expr(prog.left)) + right = self.float_deal_with_none(self.expr(prog.right)) + return '1' if self.INFIX_NUMERIC_OPS[prog.operator](left, right) else '' + except: + self.error(_('Value used in comparison is not a number. Operator {0}').format(prog.operator)) def do_node_if(self, prog): test_part = self.expr(prog.condition) @@ -406,14 +445,15 @@ class _Interpreter(object): return t NODE_OPS = { - Node.NODE_IF: do_node_if, - Node.NODE_ASSIGN: do_node_assign, - Node.NODE_CONSTANT: do_node_constant, - Node.NODE_RVALUE: do_node_rvalue, - Node.NODE_FUNC: do_node_func, - Node.NODE_FIELD: do_node_field, - Node.NODE_RAW_FIELD:do_node_raw_field, - Node.NODE_INFIX: do_node_infix, + Node.NODE_IF: do_node_if, + Node.NODE_ASSIGN: do_node_assign, + Node.NODE_CONSTANT: do_node_constant, + Node.NODE_RVALUE: do_node_rvalue, + Node.NODE_FUNC: do_node_func, + Node.NODE_FIELD: do_node_field, + Node.NODE_RAW_FIELD: do_node_raw_field, + Node.NODE_STRING_INFIX: do_node_string_infix, + Node.NODE_NUMERIC_INFIX: do_node_numeric_infix, } def expr(self, prog): @@ -494,17 +534,17 @@ class TemplateFormatter(string.Formatter): # ################# 'Functional' template language ###################### lex_scanner = re.Scanner([ - (r'(==#|!=#|<=#|<#|>=#|>#|==|!=|<=|<|>=|>)', - lambda x,t: (_Parser.LEX_INFIX, t)), - (r'(if|then|else|fi)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa - (r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa - (r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa - (r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa - (r'\w+', lambda x,t: (_Parser.LEX_ID, t)), # noqa - (r'".*?((?=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)), + (r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), + (r'(if|then|else|fi)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa + (r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa + (r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa + (r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa + (r'\w+', lambda x,t: (_Parser.LEX_ID, t)), # noqa + (r'".*?((?