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,

This commit is contained in:
Charles Haley 2020-09-14 00:59:38 +01:00
parent aa1482d782
commit fb9a388c08
2 changed files with 89 additions and 49 deletions

View File

@ -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 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 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. * ``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: 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:

View File

@ -23,10 +23,11 @@ class Node(object):
NODE_IF = 2 NODE_IF = 2
NODE_ASSIGN = 3 NODE_ASSIGN = 3
NODE_FUNC = 4 NODE_FUNC = 4
NODE_INFIX = 5 NODE_STRING_INFIX = 5
NODE_CONSTANT = 6 NODE_NUMERIC_INFIX = 6
NODE_FIELD = 7 NODE_CONSTANT = 7
NODE_RAW_FIELD = 8 NODE_FIELD = 8
NODE_RAW_FIELD = 9
class IfNode(Node): class IfNode(Node):
@ -54,10 +55,19 @@ class FunctionNode(Node):
self.expression_list = expression_list self.expression_list = expression_list
class InfixNode(Node): class StringInfixNode(Node):
def __init__(self, operator, left, right): def __init__(self, operator, left, right):
Node.__init__(self) 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.operator = operator
self.left = left self.left = left
self.right = right self.right = right
@ -96,8 +106,9 @@ class _Parser(object):
LEX_ID = 2 LEX_ID = 2
LEX_CONST = 3 LEX_CONST = 3
LEX_EOF = 4 LEX_EOF = 4
LEX_INFIX = 5 LEX_STRING_INFIX = 5
LEX_KEYWORD = 6 LEX_NUMERIC_INFIX = 6
LEX_KEYWORD = 7
def error(self, message): def error(self, message):
try: try:
@ -131,9 +142,15 @@ class _Parser(object):
except: except:
return False return False
def token_op_is_infix_compare(self): def token_op_is_string_infix_compare(self):
try: 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: except:
return False return False
@ -251,10 +268,13 @@ class _Parser(object):
def infix_expr(self): def infix_expr(self):
left = self.expr() left = self.expr()
if not self.token_op_is_infix_compare(): if self.token_op_is_string_infix_compare():
return left
operator = self.token() operator = self.token()
return InfixNode(operator, left, self.expr()) 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): def expr(self):
if self.token_is_if(): if self.token_is_if():
@ -324,25 +344,44 @@ class _Interpreter(object):
val = self.expr(p) val = self.expr(p)
return val 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: 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): def do_node_string_infix(self, prog):
try:
left = self.expr(prog.left) left = self.expr(prog.left)
right = self.expr(prog.right) right = self.expr(prog.right)
return '1' if self.INFIX_OPS[prog.operator](left, right) else '' 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): def do_node_if(self, prog):
test_part = self.expr(prog.condition) test_part = self.expr(prog.condition)
@ -413,7 +452,8 @@ class _Interpreter(object):
Node.NODE_FUNC: do_node_func, Node.NODE_FUNC: do_node_func,
Node.NODE_FIELD: do_node_field, Node.NODE_FIELD: do_node_field,
Node.NODE_RAW_FIELD: do_node_raw_field, Node.NODE_RAW_FIELD: do_node_raw_field,
Node.NODE_INFIX: do_node_infix, Node.NODE_STRING_INFIX: do_node_string_infix,
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
} }
def expr(self, prog): def expr(self, prog):
@ -494,8 +534,8 @@ class TemplateFormatter(string.Formatter):
# ################# 'Functional' template language ###################### # ################# 'Functional' template language ######################
lex_scanner = re.Scanner([ lex_scanner = re.Scanner([
(r'(==#|!=#|<=#|<#|>=#|>#|==|!=|<=|<|>=|>)', (r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)),
lambda x,t: (_Parser.LEX_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'(if|then|else|fi)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
(r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa (r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa
(r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa (r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa