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 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:

View File

@ -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
@ -96,8 +106,9 @@ class _Parser(object):
LEX_ID = 2
LEX_CONST = 3
LEX_EOF = 4
LEX_INFIX = 5
LEX_KEYWORD = 6
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
if self.token_op_is_string_infix_compare():
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):
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):
def do_node_string_infix(self, prog):
try:
left = self.expr(prog.left)
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):
test_part = self.expr(prog.condition)
@ -413,7 +452,8 @@ class _Interpreter(object):
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_STRING_INFIX: do_node_string_infix,
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
}
def expr(self, prog):
@ -494,8 +534,8 @@ class TemplateFormatter(string.Formatter):
# ################# 'Functional' template language ######################
lex_scanner = re.Scanner([
(r'(==#|!=#|<=#|<#|>=#|>#|==|!=|<=|<|>=|>)',
lambda x,t: (_Parser.LEX_INFIX, t)),
(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