StringEvaluation
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RicardoSantos

//@version=5

// @description Methods to handle evaluation of strings.
library(title='StringEvaluation')

// imports:
import RicardoSantos/MathOperator/1 as op
import RicardoSantos/DebugConsole/2 as console
[__T, __C] = console.init(20)

// Helper Functions:

// @function Check if char is a comma ".".
// @param char string, 1 character string.
// @returns bool.
export is_comma (string char) => //{
    char == '.' ? true : false
//}

// @function Check if char is a operator component.
// @param char string, 1 character string.
// @returns bool.
export is_op_component (string char) => //{
    switch char
        '=' => true
        '!' => true
        '>' => true
        '<' => true
        => false
//}

// @function Check if char is a boolean operator.
// @param char string, 1 character string.
// @returns bool.
export is_boolean_op (string char) => //{
    switch char
        '==' => true
        '!=' => true
        '>=' => true
        '<=' => true
        '>' => true
        '<' => true
        'and' => true
        'or' => true
        => false
//}

// @function Check if char is a operator.
// @param char string, 1 character string.
// @returns bool.
export is_op (string char) => //{
    switch char
        '+' => true
        '-' => true
        '*' => true
        '/' => true
        '^' => true
        '%' => true
        // booleans
        '==' => true
        '!=' => true
        '>=' => true
        '<=' => true
        '>' => true
        '<' => true
        'and' => true
        'or' => true
        => false
//}

// @function Check if char is alphabet.
// @param char string, 1 character string.
// @returns bool.
export is_alpha (string char) => //{
    switch char
        'a' => true
        'b' => true
        'c' => true
        'd' => true
        'e' => true
        'f' => true
        'g' => true
        'h' => true
        'i' => true
        'j' => true
        'k' => true
        'l' => true
        'm' => true
        'n' => true
        'o' => true
        'p' => true
        'q' => true
        'r' => true
        's' => true
        't' => true
        'u' => true
        'v' => true
        'w' => true
        'x' => true
        'y' => true
        'z' => true
        => false
//}

// @function convert a single char string into valid number.
// @param char string, 1 character string.
// @returns float.
export number (string char) => //{
    switch char
        '0' => 0
        '1' => 1
        '2' => 2
        '3' => 3
        '4' => 4
        '5' => 5
        '6' => 6
        '7' => 7
        '8' => 8
        '9' => 9
        => na
//}

// @function boolean operation between left and right values.
// @param op string, operator string character.
// @param left float, left value of operation.
// @param right float, right value of operation.
export boolean_operator (string op, float left, float right) => //{
    switch op
        '==' => op.equal(left, right)
        '!=' => op.not_equal(left, right)
        '>=' => op.over_equal(left, right)
        '<=' => op.under_equal(left, right)
        '>' => op.over(left, right)
        '<' => op.under(left, right)
        'and' => op.and_(left, right)
        'or' => op.or_(left, right)
        => na
//}

// @function operation between left and right values.
// @param op string, operator string character.
// @param left float, left value of operation.
// @param right float, right value of operation.
export operator (string op, float left, float right) => //{
    switch op
        '+' => op.add(left, right)
        '-' => op.subtract(left, right)
        '*' => op.multiply(left, right)
        '/' => op.divide(left, right)
        '^' => math.pow(left, right)
        '%' => op.remainder(left, right)
        // booleans
        '==' => op.equal(left, right) ? 1 : 0
        '!=' => op.not_equal(left, right) ? 1 : 0
        '>=' => op.over_equal(left, right) ? 1 : 0
        '<=' => op.under_equal(left, right) ? 1 : 0
        '>' => op.over(left, right) ? 1 : 0
        '<' => op.under(left, right) ? 1 : 0
        'and' => op.and_(left, right) ? 1 : 0
        'or' => op.or_(left, right) ? 1 : 0
        => na
//}

// @function level of precedence of operator.
// @param op string, operator 1 char string.
// @returns int.
export operator_precedence (string op) => //{
    switch op
        '+' => 3
        '-' => 3
        '*' => 4
        '/' => 4
        '^' => 5
        '%' => 4
        // booleans
        '==' => 2
        '!=' => 2
        '>=' => 2
        '<=' => 2
        '>' => 2
        '<' => 2
        'and' => 1
        'or' => 1
        => 0
//}

// @function level of precedence of operator.
// @param op string, operator 1 char string.
// @returns int.
export boolean_operator_precedence (string op) => //{
    switch op
        '==' => 2
        '!=' => 2
        '>=' => 2
        '<=' => 2
        '>' => 2
        '<' => 2
        'and' => 1
        'or' => 1
        => 0
//}

// @function Aggregates words, numbers and operators into one.
// @param tokens string array, array with split string into character tokens.
// @returns string array.
export aggregate_words (string[] tokens) => //{
    int _size_t = array.size(id=tokens)
    //
    switch
        (_size_t < 1) => runtime.error('StringEvaluation -> aggregate_words(): "tokens" is empty.')
    //
    string[] _tokens = array.copy(tokens)
    for _i = _size_t - 1 to 1
        string _current = array.get(_tokens, _i)
        string _previous = array.get(_tokens, _i-1)
        //add to number
        if (str.tonumber(_current) or is_comma(_current)) and (str.tonumber(_previous) or is_comma(_previous))
            array.set(_tokens, _i-1, _previous+_current)
            array.remove(_tokens, _i)
            // console.queue(__C, str.tostring(_tokens))
        if (is_alpha(array.get(str.split(_current, ''), 0)) or is_comma(_current)) and (is_alpha(_previous) or is_comma(_previous))
            array.set(_tokens, _i-1, _previous+_current)
            array.remove(_tokens, _i)
            // console.queue(__C, str.tostring(_tokens))
        if (is_op_component(array.get(str.split(_current, ''), 0))) and (is_op_component(_previous))
            array.set(_tokens, _i-1, _previous+_current)
            array.remove(_tokens, _i)
            // console.queue(__C, str.tostring(_tokens))
    _tokens
// aggregate_words(str.split('123 a 321 bb cc 123. a .123 + 1.23 - 12.3 | .a | b. | a.b', ''))
//}

// @function Evaluate a string to clean up and retrieve only used chars
// @param _str string, arithmetic operations in a string.
// @returns string array, evaluated array.
export cleanup (string _str) => //{
    // remove spaces:
    _unspaced = str.replace_all(source=_str, target=' ', replacement='')
    // aggregate numbers and words:
    _T0 = aggregate_words(str.split(_unspaced, ''))
    //
    // adds ( ) + - / * characters:
    // join digit sequences into 1 whole number:
    _T1 = array.new_string(0)
    string _saved_expression = ''
    for _i = 0 to array.size(id=_T0)-1
        _state = 0
        _current = array.get(id=_T0, index=_i)
        if _current == '(' or _current == ')'
            if _saved_expression != ''
                array.push(id=_T1, value=_saved_expression)
            array.push(id=_T1, value=_current)
            _saved_expression := ''
            _state := 0
        if (not na(operator(_current, 1, 1)))
            if _saved_expression != ''
                array.push(id=_T1, value=_saved_expression)
            array.push(id=_T1, value=_current)
            _saved_expression := ''
            _state := 0
        // save words:
        if _state <= 0 and na(str.tonumber(_saved_expression)) and is_alpha(_current)
            _saved_expression += _current
            _state := -1
        // save numbers:
        if _state >= 0 and (not na(number(_current)) or is_comma(_current))
            _saved_expression += _current
            _state := 1
    array.push(id=_T1, value=_saved_expression)
    _T1
//{ usage:
// cleanup_a = '1+ 66* 7/2^2  %5 '
// cleanup_b = '0 > 1 and 1> 0 >= 2 <= 1 or 1 == 0 != 1'
// console.queue_one(__C, str.format('cleanup "{0}": "{1}"', cleanup_a, str.tostring(cleanup(cleanup_a))))
// console.queue_one(__C, str.format('cleanup "{0}": "{1}"', cleanup_b, str.tostring(cleanup(cleanup_b))))
//}

// @function uses Shunting-Yard algorithm to generate a RPN (Reverse Polish notation)
//      array of strings from a array of strings containing arithmetic notation.
//      ex:.. '[3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3]'     -->     '[3 4 2 * 1 5 - 2 3 ^ ^ / +]'
// @param tokens string array, array with arithmetic notation.
// @returns 
export generate_rpn (string[] tokens)=> //{
    int _size = array.size(id=tokens)
    if _size < 1
        runtime.error('StringEvaluation -> generate_rpn(): "tokens" is empty.')
        string[] _S = array.new_string(na)
    else
        _opstack = array.new_string(size=0)
        _output = array.new_string(size=0)
        for _i = 0 to _size-1
            string _current_token = array.get(id=tokens, index=_i)
            _current_arrified = str.split(_current_token, '')
            int _current_size = array.size(id=_current_arrified)
            if not na(number(array.get(id=_current_arrified, index=0)))
                array.push(id=_output, value=_current_token)
            else if _current_token == '('
                array.push(id=_opstack, value=_current_token)
            else if _current_token == ')'
                int _size_stack = array.size(id=_opstack)
                if _size_stack > 0
                    for _j = _size_stack-1 to 0
                        string _op = array.pop(id=_opstack)
                        if _op == '('
                            break
                        else
                            array.push(id=_output, value=_op)
            else if is_op(_current_token)
                int _size_stack = array.size(id=_opstack)
                if _size_stack > 0
                    for _j = _size_stack-1 to 0
                        string _op = array.pop(id=_opstack)
                        int _op_precedence = operator_precedence(_op)
                        int _token_precedence = operator_precedence(_current_token)
                        if _op_precedence > _token_precedence or (_op_precedence == _token_precedence and _op != '^')
                            array.push(id=_output, value=_op)
                        else
                            array.push(id=_opstack, value=_op)
                            break
                array.push(id=_opstack, value=_current_token)
        int _size_stack = array.size(id=_opstack)
        if _size_stack > 0
            for _i = _size_stack-1 to 0
                string _op = array.pop(id=_opstack)
                if _op == '('
                    runtime.error('StringEvaluation -> generate_rpn(): mismatched parentesis!.')
                else
                    array.push(id=_output, value=_op)
        _output
//{ usage:
// if barstate.islastconfirmedhistory
//     // label.new(bar_index, 0.0, str.tostring(generate_rpn(cleanup('1+ 66* 7/2^2  %5 '))))
//     label.new(bar_index, 0.0, str.tostring(generate_rpn(cleanup('1 == 1'))))
//{ reference:
//     https://rosettacode.org/wiki/Parsing/Shunting-yard_algorithm
//     https://en.wikipedia.org/wiki/Shunting-yard_algorithm
//     https://brilliant.org/wiki/shunting-yard-algorithm/
//}}}

// @function evaluate a RPN (Reverse Polish notation) array of strings.
//      ex:.. 3 4 2 * 1 5 - 2 3 ^ ^ / +
//| @param tokens string array, RPN ordered tokens, ex(['3', '4', '2', '1', '5', '2', '3', '*', '-', '^', '^', '/', '+']).
//| @returns float, solution.
export parse_rpn (string[] tokens) => //{
    int _size = array.size(id=tokens)
    if _size < 1
        runtime.error('StringEvaluation -> generate_rpn(): "tokens" is empty.')
        float(na)
    else
        float _solution = na
        _stack = array.new_float()
        for _i = 0 to _size-1
            string _token = array.get(id=tokens, index=_i)
            float _number = str.tonumber(_token)
            if not na(_number)//
                array.push(id=_stack, value=_number)
            else
                if array.size(id=_stack) > 1
                //     runtime.error('StringEvaluation -> generate_rpn(): insufficient operands!!.\n' + str.tostring(_stack))
                // else
                    o2 = array.pop(id=_stack)
                    o1 = array.pop(id=_stack)
                    if is_op(_token)
                        float _op = operator(_token, o1, o2)
                        array.push(id=_stack, value=_op)
                    else
                        runtime.error('StringEvaluation -> generate_rpn(): unrecognized operator: (' + _token + ' ).')
        _solution := array.pop(id=_stack)
        _solution
//{ usage:
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, str.tostring(parse_rpn(generate_rpn(cleanup('((10+ 10 *30/20-50)%10)^2')))))
//{ reference:
//     https://rosettacode.org/wiki/Parsing/RPN_calculator_algorithm
//}}}

// @function evaluate a string with references to a array of arguments.
//| @param tokens string, arithmetic operations with references to indices in arguments, ex:"0+1*0+2*2+3" arguments[1, 2, 3]
//| @param arguments float array, arguments.
//| @returns float, solution.
export eval (string tokens, float[] arguments) => //{
    int _size_a = array.size(id=arguments)
    if _size_a < 1
        runtime.error('StringEvaluation -> generate_rpn(): "arguments" is empty.')
        float(na)
    else
        string[] _tokens = generate_rpn(cleanup(tokens))
        int _size_t = array.size(id=_tokens)
        if _size_t < 1
            runtime.error('StringEvaluation -> generate_rpn(): "tokens" is empty.')
            float(na)
        else
            // calculate solution:
            float _solution = na
            _stack = array.new_float()
            for _i = 0 to _size_t-1
                string _token = array.get(id=_tokens, index=_i)
                float _number = str.tonumber(_token)
                if not na(_number)//
                    array.push(id=_stack, value=array.get(id=arguments, index=int(_number)))
                else
                    if array.size(id=_stack) > 1
                    //     runtime.error('StringEvaluation -> generate_rpn(): insufficient operands!!.\n' + str.tostring(_stack))
                    // else
                        o2 = array.pop(id=_stack)
                        o1 = array.pop(id=_stack)
                        if is_op(_token)
                            float _op = operator(_token, o1, o2)
                            array.push(id=_stack, value=_op)
                        else
                            runtime.error('StringEvaluation -> generate_rpn(): unrecognized operator: (' + _token + ' ).')
            _solution := array.pop(id=_stack)
            _solution
//{ usage:
console.queue_one(__C, str.format('(((10+ 10 *30/20-50)%10)^2) = {0}', str.tostring(eval('(((0+ 0 *2/1-3)%0)^4)', array.from(10.0, 20, 30, 50, 2)))))
console.queue_one(__C, str.format('20 > 10 = {0}', str.tostring(eval('1 > 0', array.from(10.0, 20, 30, 50, 2)))))
console.queue_one(__C, str.format('20 >= 20 = {0}', str.tostring(eval('1 >= 1', array.from(10.0, 20, 30, 50, 2)))))
console.queue_one(__C, str.format('20 and 10 = {0}', str.tostring(eval('1 and 0', array.from(10.0, 20, 30, 50, 2)))))
console.queue_one(__C, str.format('20 or 10 = {0}', str.tostring(eval('1 or 0', array.from(10.0, 20, 30, 50, 2)))))
console.queue_one(__C, str.format('((20 <= 10 or 20 > 10) + (20 > 10)) * 25.33 = {0}', str.tostring(eval('((1 <= 0 or 1 > 0) + (1 > 0)) * 3', array.from(10.0, 20, 30, 25.33, 2)))))
//{ reference:
//     https://rosettacode.org/wiki/Parsing/RPN_calculator_algorithm
//}}}


console.update(__T, __C)