Source code for bTagScript.block.math_blocks

from __future__ import division

import math
import operator
from typing import Optional as Optional_

from pyparsing import (
    CaselessLiteral,
    Combine,
    Forward,
    Group,
    Literal,
    Optional,
    Word,
    ZeroOrMore,
    alphas,
    nums,
    oneOf,
)

from ..interface import Block
from ..interpreter import Context


class NumericStringParser(object):
    """
    Most of this code comes from the fourFn.py pyparsing example

    """

    def pushFirst(self, strg: str, loc, toks) -> None:  # pylint: disable=unused-argument
        """
        Parse actions that push the first element of the matched tokens
        """
        self.exprStack.append(toks[0])

    def pushUMinus(self, strg: str, loc, toks) -> None:  # pylint: disable=unused-argument
        """
        Parse actions that push the last element of the matched tokens??
        """
        if toks and toks[0] == "-":
            self.exprStack.append("unary -")

    def __init__(self) -> None:
        """
        expop   :: '^'
        multop  :: '*' | '/'
        addop   :: '+' | '-'
        integer :: ['+' | '-'] '0'..'9'+
        atom    :: PI | E | real | fn '(' expr ')' | '(' expr ')'
        factor  :: atom [ expop factor ]*
        term    :: factor [ multop factor ]*
        expr    :: term [ addop term ]*
        """
        self.exprStack = []
        point = Literal(".")
        e = CaselessLiteral("E")
        fnumber = Combine(
            Word("+-" + nums, nums)
            + Optional(point + Optional(Word(nums)))
            + Optional(e + Word("+-" + nums, nums))
        )
        ident = Word(alphas, alphas + nums + "_$")
        mod = Literal("%")
        plus = Literal("+")
        minus = Literal("-")
        mult = Literal("*")
        iadd = Literal("+=")
        imult = Literal("*=")
        idiv = Literal("/=")
        isub = Literal("-=")
        div = Literal("/")
        lpar = Literal("(").suppress()
        rpar = Literal(")").suppress()
        addop = plus | minus
        multop = mult | div | mod
        iop = iadd | isub | imult | idiv
        expop = Literal("^")
        pi = CaselessLiteral("PI")
        expr = Forward()
        atom = (
            (
                Optional(oneOf("- +"))
                + (ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst)
            )
            | Optional(oneOf("- +")) + Group(lpar + expr + rpar)
        ).setParseAction(self.pushUMinus)
        # by defining exponentiation as "atom [ ^ factor ]..." instead of
        # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
        # that is, 2^3^2 = 2^(3^2), not (2^3)^2.
        factor = Forward()
        factor << atom + ZeroOrMore(  # pylint: disable=expression-not-assigned
            (expop + factor).setParseAction(self.pushFirst)
        )
        term = factor + ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
        expr << term + ZeroOrMore(  # pylint: disable=expression-not-assigned
            (addop + term).setParseAction(self.pushFirst)
        )
        final = expr + ZeroOrMore((iop + expr).setParseAction(self.pushFirst))
        # addop_term = ( addop + term ).setParseAction( self.pushFirst )
        # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
        # expr <<  general_term
        self.bnf = final
        # map operator symbols to corresponding arithmetic operations
        epsilon = 1e-12
        self.opn = {
            "+": operator.add,
            "-": operator.sub,
            "+=": operator.iadd,
            "-=": operator.isub,
            "*": operator.mul,
            "*=": operator.imul,
            "/": operator.truediv,
            "/=": operator.itruediv,
            "^": operator.pow,
            "%": operator.mod,
        }
        self.fn = {
            "sin": math.sin,
            "cos": math.cos,
            "tan": math.tan,
            "exp": math.exp,
            "abs": abs,
            "trunc": lambda a: int(a),  # pylint: disable=unnecessary-lambda
            "round": round,
            "sgn": lambda a: abs(a) > epsilon and ((a > 0) - (a < 0)) or 0,
            "log": lambda a: math.log(a, 10),
            "ln": math.log,
            "log2": math.log2,
            "sqrt": math.sqrt,
        }

    def evaluateStack(self, s):
        """
        Evaluate the expression on the input data in the stack.
        """
        op = s.pop()
        if op == "unary -":
            return -self.evaluateStack(s)
        if op in self.opn:
            op2 = self.evaluateStack(s)
            op1 = self.evaluateStack(s)
            return self.opn[op](op1, op2)
        elif op == "PI":
            return math.pi  # 3.1415926535
        elif op == "E":
            return math.e  # 2.718281828
        elif op in self.fn:
            return self.fn[op](self.evaluateStack(s))
        elif op[0].isalpha():
            return 0
        else:
            return float(op)

    def eval(self, num_string: str, parseAll: bool = True) -> float:
        """
        Evaluate the expression on the input data.
        """
        results = self.bnf.parseString(num_string, parseAll)  # pylint: disable=unused-variable
        return self.evaluateStack(self.exprStack[:])


NSP = NumericStringParser()


[docs]class MathBlock(Block): """ A math block is a block that contains a math expression. Will write out everything later bleh **Usage:** ``{math:<expression>}`` **Aliases:** ``math, m, +, calc`` **Payload:** ``expression`` **Parameter:** None **Examples:** .. tagscript:: {m:2+3} 5.0 {math:7(2+3)} 42.0 {math:trunc(7(2+3))} 42 """ ACCEPTED_NAMES = ("math", "m", "+", "calc")
[docs] def process(self, ctx: Context) -> Optional_[str]: """ Try and process the block into a float """ try: return str(NSP.eval(ctx.verb.payload.strip(" "))) except: # pylint: disable=bare-except return None
[docs]class OrdinalAbbreviationBlock(Block): """ The ordinalabbreviation block returns the ordinal abbreviation of a number. If a parameter is provided, it must be, one of, c, comma, indicator, i Comma being adding commas every 3 digits, indicator, meaning the ordinal indicator. (The st of 1st, nd of 2nd, etc.) The number may be positive or negative, if the payload is invalid, -1 is returned. **Usage:** ``{ord(["c", "comma", "i", "indicator"]):<number>}`` **Aliases:** ``None`` **Payload:** ``number`` **Parameter:** ``"c", "comma", "i", "indicator"`` .. tagscript:: {ord:1000} 1,000th {ord(c):1213123} 1,213,123 {ord(i):2022} 2022nd """ ACCEPTED_NAMES = ("ord",)
[docs] def process(self, ctx: Context) -> str: """ Process the ordinal abbreviation block """ num = ctx.verb.payload.split("-", 1)[-1] if num.isdigit(): comma = f"{int(num):,}" if ctx.verb.parameter in ["c", "comma"]: return comma i = int(ctx.verb.payload.split("-", 1)[-1]) indicator = "tsnrhtdd"[ (i // 10 % 10 != 1) * (i % 10 < 4) * i % 10 :: 4 ] # I stole this from stack overflow if ctx.verb.parameter in ["i", "indicator"]: return f"{ctx.verb.payload}{indicator}" # concatenation is slower? return f"{comma}{indicator}" return "-1"