import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import re
import numpy as np

nltk.download("stopwords", quiet=True)
nltk.download("punkt", quiet=True)


import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import re
import numpy as np

nltk.download("stopwords", quiet=True)
nltk.download("punkt", quiet=True)


def parse_sentence(user_input):
    """Tokenizes the user_input, gets rid of the stop words and returns a list of tokenized string parts.

    INPUTS
    =======
    user_input: a string which typically includes some form of equation, variables and values

    RETURNS
    ========
    list: a list of tokenized parts of the sentence

    EXAMPLES
    =========
    >>> parse_sentence('x^2 + 2x when x is 1')
    >>> ['x^2', '+', '2x', 'x', '1']

    """

    stop_words = np.array(stopwords.words("english"))  ### get the english stipwords
    stop_words_set = set(
        stopwords.words("english")
    )  ### take out the single letters since they might be variables
    for s in stop_words:
        if len(s) == 1:
            stop_words_set.remove(s)
    stop_words_set.add(
        "="
    )  ### add '=' for the case when the user inputs "... where x=2, y=3"

    ### tokenize the user input and take out the stop words
    word_tokens = word_tokenize(user_input)
    filtered_sentence = [w for w in word_tokens if not w in stop_words_set]
  

    return filtered_sentence

def flatten(l):
    """This function takes a list and flattens it if there are any other list elements in it

    INPUTS
    =======
    l: any list

    RETURNS
    ========
    list: flattened list
    

    EXAMPLES
    =========
    >>> flatten(['2', '+', 'sin', '(', 'x', ')', '-', '3y', ['x', '2'], ['y', '3']])
    >>> ['2', '+', 'sin', '(', 'x', ')', '-', '3y', 'x', '2', 'y', '3']

    """ 
    
    flattened = []
    for x in l:
        if isinstance(x, list):
            flattened.extend(x)
        else:
            flattened.append(x)
    
    return flattened

def no_unwanted_symbols(filtered_sentence):
    """Cleans the string further. Gets rid of equal signs and some unneccessary punctuation.

    INPUTS
    =======
    filtered_sentence: a list of tokenized parts of the user input

    RETURNS
    ========
    list: a list of tokenized parts of the sentence but all cleaned

    EXAMPLES
    =========
    >>> no_unwanted_symbols('x^2 + 2x - 3y when x= 1, y=3')
    >>> ['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3']

    """
    
    for i in range(len(filtered_sentence)):
        
        a = filtered_sentence[i]
        ## get rid of comma
        filtered_sentence[i] = a.replace(',', '')

        ## take out the equal signs
        if '=' in a:
            splitted = a.split('=')
            filtered_sentence[i] = splitted
            
    ## make sure there are no empty strings left in the list
    
    filtered_sentence = [i for i in filtered_sentence if i != '']
    filtered_sentence = flatten(filtered_sentence)

    return filtered_sentence

def get_variables(filtered_sentence):
    """Inspects the tokenized list and finds the variables.

    INPUTS
    =======
    filtered_sentence: a list of tokenized parts of the user input

    RETURNS
    ========
    list: a list of variables
    string: the unclean equation, essentially, the version of the first input without the stop words and all joined

    EXAMPLES
    =========
    >>> get_variables(['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3'])
    >>> (['x', 'y'], 'x^2+2x-3yx1y3')

    """

    letter_ops = ['sin', 'cos', 'e', 'exp', 'tan', 'arctan', 'arcsin', 'arccos']
    filtered_sent_copy = filtered_sentence.copy()
    for i in letter_ops:
        if i in filtered_sent_copy:
            filtered_sent_copy.remove(i)
    unclean_eq = "".join(filtered_sent_copy)
    var_regex = r"[^\d  * \.|+-:/\^ \\( \\ )=]"
    variables = np.unique(re.findall( var_regex, unclean_eq))

    return variables, unclean_eq


def get_eq_and_vals(filtered_sentence, variables, unclean_eq):
    """Parses and separates the equation part from the values in the string

    INPUTS
    =======
    filtered_sentence: a list of tokenized strings from the original equation
    variables: list of the user defined variables
    unclean_eq: essentially the version of the first input without stop the words

    RETURNS
    ========
    string: the equation part
    list: the variables and the values in the list

    EXAMPLES
    =========  
    >>> filtered_sentence = parse_sentence('x^2 + 2x when x is 1')
    >>> get_eq_and_vals(filtered_sentence, ['x', 'y'], 'x^2+2xx1')
    >>> ('x^2 + 2x', ['x', '1'])

    """
    regex_vars = ''
    for var in variables:
        regex_vars += var + '\d+\.*\d*'
        

    var_val_str = re.findall(regex_vars,unclean_eq)[0] 
    print('vvs', var_val_str)
    values = [i for i in re.findall('\d*\.*\d*', var_val_str) if i !='']
    print('vars_vals', values)
    equation = ''
    final_eq = ''.join(filtered_sentence)
    for i in final_eq.split(var_val_str):
        if i != '':
            equation = i
    
    return equation, values



def get_values(variables, vars_vals):
    """Returns a dictionary where the keys are the variable names and the values are the variable values

    INPUTS
    =======
    variables: variables that are filtered via regex from the equation
    vars_vals: the values filtered from the input

    RETURNS
    ========
    dictionary: the keys are the variable names and the values are the variable values

    EXAMPLES
    =========
    >>> variables = ['x']
    >>> vars_vals = ['1']
    >>> get_values(variables, vars_vals)
    >>> {'x': '1'}

    """

    values = {}
    variables_list = list(variables)
    for item in variables_list:
        ind_item = variables_list.index(item)
        values[item] = float(vars_vals[ind_item])
      
    return values

def split(word):
    """This function takes a string and returns its single characters in the list

    INPUTS
    =======
    word: any string

    RETURNS
    ========
    list: a list of the characters of the word
    

    EXAMPLES
    =========
    >>> split('word')
    >>> ['w', 'o', 'r', 'd']

    """ 
    return [char for char in word]


def pythonize(eq, variables):
    """Takes in the equation part of the string and converts everything to python executable format

    INPUTS
    =======
    eq: the equation part of the user defined string
    variables: a list of variable names

    RETURNS
    ========
    string: pythonic equation

    EXAMPLES
    =========
    >>> pythonize('2^y + sin(7x)', ['x', 'y'])
    >>> '2**y + sin(7*x)'

    """ 
    
    ### take care of the exponents
    if 'e^' in eq:
        eq = eq.replace('e' , 'np.e')
        
    ### take care of division, power
    eq = eq.replace('^', '**')
    eq = eq.replace(':', '/')
    eq = eq.replace(',', '')
    

    ### take care of the multiplication (ex: xy, 7y, 6xz)
    varstring = ''.join(variables)
    opstring = '+-\\|\\|*\/:\\^' 
    
    regex = f"([0-9.]*[{varstring}]+)"
    matches = re.findall(regex, eq)
    
    for mat in np.unique(matches):
        if len(mat)>1:
            
            splitted = split(mat)
            correct_expr = '*'.join(splitted)
        
            eq = re.sub(mat, correct_expr, eq)
    
    return eq



def pipeline(user_input):
    
    """This function takes care of the whole parsing pipeline

    INPUTS
    =======
    user_input: a string which typically includes some form of equation, variables and values

    RETURNS
    ========
    string: final clean pythonic equation
    dictionary: the keys are the variable names and the values are the variable values

    EXAMPLES
    =========
    >>> pipeline('x^2 + y- 7xy where x is 2 and y is 8')
    >>> ('x**2+y-7*x*y', {'x': '2', 'y': '8'})

    """ 
    
    ### tokenize and parse the sentence
    filtered_sentence = parse_sentence(user_input)
    filtered_sentence = no_unwanted_symbols(filtered_sentence)
    
    #get the variables and the unclean equation 
    variables, unclean_eq = get_variables(filtered_sentence)
    #clean the equation and get the values for the variables
    equation, vars_vals = get_eq_and_vals(filtered_sentence, variables, unclean_eq)
 
    ### gets the variables and values in the dictionary format
    val_dict = get_values(variables, vars_vals)
    
    ### clean and pythonize the equation
    eq = pythonize(equation, variables)      
    
    return eq, val_dict



import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import re
import numpy as np

nltk.download("stopwords", quiet=True)
nltk.download("punkt", quiet=True)


import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import re
import numpy as np

nltk.download("stopwords", quiet=True)
nltk.download("punkt", quiet=True)


def parse_sentence(user_input):
    """Tokenizes the user_input, gets rid of the stop words and returns a list of tokenized string parts.

    INPUTS
    =======
    user_input: a string which typically includes some form of equation, variables and values

    RETURNS
    ========
    list: a list of tokenized parts of the sentence

    EXAMPLES
    =========
    >>> parse_sentence('x^2 + 2x when x is 1')
    >>> ['x^2', '+', '2x', 'x', '1']

    """

    stop_words = np.array(stopwords.words("english"))  ### get the english stipwords
    stop_words_set = set(
        stopwords.words("english")
    )  ### take out the single letters since they might be variables
    for s in stop_words:
        if len(s) == 1:
            stop_words_set.remove(s)
    stop_words_set.add(
        "="
    )  ### add '=' for the case when the user inputs "... where x=2, y=3"

    ### tokenize the user input and take out the stop words
    word_tokens = word_tokenize(user_input)
    filtered_sentence = [w for w in word_tokens if not w in stop_words_set]
  

    return filtered_sentence

def flatten(l):
    """This function takes a list and flattens it if there are any other list elements in it

    INPUTS
    =======
    l: any list

    RETURNS
    ========
    list: flattened list
    

    EXAMPLES
    =========
    >>> flatten(['2', '+', 'sin', '(', 'x', ')', '-', '3y', ['x', '2'], ['y', '3']])
    >>> ['2', '+', 'sin', '(', 'x', ')', '-', '3y', 'x', '2', 'y', '3']

    """ 
    
    flattened = []
    for x in l:
        if isinstance(x, list):
            flattened.extend(x)
        else:
            flattened.append(x)
    
    return flattened

def no_unwanted_symbols(filtered_sentence):
    """Cleans the string further. Gets rid of equal signs and some unneccessary punctuation.

    INPUTS
    =======
    filtered_sentence: a list of tokenized parts of the user input

    RETURNS
    ========
    list: a list of tokenized parts of the sentence but all cleaned

    EXAMPLES
    =========
    >>> no_unwanted_symbols('x^2 + 2x - 3y when x= 1, y=3')
    >>> ['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3']

    """
    
    for i in range(len(filtered_sentence)):
        
        a = filtered_sentence[i]
        ## get rid of comma
        filtered_sentence[i] = a.replace(',', '')

        ## take out the equal signs
        if '=' in a:
            splitted = a.split('=')
            filtered_sentence[i] = splitted
            
    ## make sure there are no empty strings left in the list
    
    filtered_sentence = [i for i in filtered_sentence if i != '']
    filtered_sentence = flatten(filtered_sentence)

    return filtered_sentence

def get_variables(filtered_sentence):
    """Inspects the tokenized list and finds the variables.

    INPUTS
    =======
    filtered_sentence: a list of tokenized parts of the user input

    RETURNS
    ========
    list: a list of variables
    string: the unclean equation, essentially, the version of the first input without the stop words and all joined

    EXAMPLES
    =========
    >>> get_variables(['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3'])
    >>> (['x', 'y'], 'x^2+2x-3yx1y3')

    """

    letter_ops = ['sin', 'cos', 'e', 'exp', 'tan', 'arctan', 'arcsin', 'arccos']
    filtered_sent_copy = filtered_sentence.copy()
    for i in letter_ops:
        if i in filtered_sent_copy:
            filtered_sent_copy.remove(i)
    unclean_eq = "".join(filtered_sent_copy)
    var_regex = r"[^\d  * \.|+-:/\^ \\( \\ )=]"
    variables = np.unique(re.findall( var_regex, unclean_eq))

    return variables, unclean_eq


def get_eq_and_vals(filtered_sentence, variables, unclean_eq):
    """Parses and separates the equation part from the values in the string

    INPUTS
    =======
    filtered_sentence: a list of tokenized strings from the original equation
    variables: list of the user defined variables
    unclean_eq: essentially the version of the first input without stop the words

    RETURNS
    ========
    string: the equation part
    list: the variables and the values in the list

    EXAMPLES
    =========  
    >>> filtered_sentence = parse_sentence('x^2 + 2x when x is 1')
    >>> get_eq_and_vals(filtered_sentence, ['x', 'y'], 'x^2+2xx1')
    >>> ('x^2 + 2x', ['x', '1'])

    """
    regex_vars = ''
    for var in variables:
        regex_vars += var + '\d+\.*\d*'
        

    var_val_str = re.findall(regex_vars,unclean_eq)[0] 
    print('vvs', var_val_str)
    values = [i for i in re.findall('\d*\.*\d*', var_val_str) if i !='']
    print('vars_vals', values)
    equation = ''
    final_eq = ''.join(filtered_sentence)
    for i in final_eq.split(var_val_str):
        if i != '':
            equation = i
    
    return equation, values



def get_values(variables, vars_vals):
    """Returns a dictionary where the keys are the variable names and the values are the variable values

    INPUTS
    =======
    variables: variables that are filtered via regex from the equation
    vars_vals: the values filtered from the input

    RETURNS
    ========
    dictionary: the keys are the variable names and the values are the variable values

    EXAMPLES
    =========
    >>> variables = ['x']
    >>> vars_vals = ['1']
    >>> get_values(variables, vars_vals)
    >>> {'x': '1'}

    """

    values = {}
    variables_list = list(variables)
    for item in variables_list:
        ind_item = variables_list.index(item)
        values[item] = float(vars_vals[ind_item])
      
    return values

def split(word):
    """This function takes a string and returns its single characters in the list

    INPUTS
    =======
    word: any string

    RETURNS
    ========
    list: a list of the characters of the word
    

    EXAMPLES
    =========
    >>> split('word')
    >>> ['w', 'o', 'r', 'd']

    """ 
    return [char for char in word]


def pythonize(eq, variables):
    """Takes in the equation part of the string and converts everything to python executable format

    INPUTS
    =======
    eq: the equation part of the user defined string
    variables: a list of variable names

    RETURNS
    ========
    string: pythonic equation

    EXAMPLES
    =========
    >>> pythonize('2^y + sin(7x)', ['x', 'y'])
    >>> '2**y + sin(7*x)'

    """ 
    
    ### take care of the exponents
    if 'e^' in eq:
        eq = eq.replace('e' , 'np.e')
        
    ### take care of division, power
    eq = eq.replace('^', '**')
    eq = eq.replace(':', '/')
    eq = eq.replace(',', '')
    

    ### take care of the multiplication (ex: xy, 7y, 6xz)
    varstring = ''.join(variables)
    opstring = '+-\\|\\|*\/:\\^' 
    
    regex = f"([0-9.]*[{varstring}]+)"
    matches = re.findall(regex, eq)
    
    for mat in np.unique(matches):
        if len(mat)>1:
            
            splitted = split(mat)
            correct_expr = '*'.join(splitted)
        
            eq = re.sub(mat, correct_expr, eq)
    
    return eq



def pipeline(user_input):
    
    """This function takes care of the whole parsing pipeline

    INPUTS
    =======
    user_input: a string which typically includes some form of equation, variables and values

    RETURNS
    ========
    string: final clean pythonic equation
    dictionary: the keys are the variable names and the values are the variable values

    EXAMPLES
    =========
    >>> pipeline('x^2 + y- 7xy where x is 2 and y is 8')
    >>> ('x**2+y-7*x*y', {'x': '2', 'y': '8'})

    """ 
    
    ### tokenize and parse the sentence
    filtered_sentence = parse_sentence(user_input)
    filtered_sentence = no_unwanted_symbols(filtered_sentence)
    
    #get the variables and the unclean equation 
    variables, unclean_eq = get_variables(filtered_sentence)
    #clean the equation and get the values for the variables
    equation, vars_vals = get_eq_and_vals(filtered_sentence, variables, unclean_eq)
 
    ### gets the variables and values in the dictionary format
    val_dict = get_values(variables, vars_vals)
    
    ### clean and pythonize the equation
    eq = pythonize(equation, variables)      
    
    return eq, val_dict



Tokenizes the user_input, gets rid of the stop words and returns a list of tokenized string parts.      INPUTS     =======     user_input: a string which typically includes some form of equation, variables and values      RETURNS     ========     list: a list of tokenized parts of the sentence      EXAMPLES     =========     >>> parse_sentence('x^2 + 2x when x is 1')     >>> ['x^2', '+', '2x', 'x', '1'] ### get the english stipwords ### take out the single letters since they might be variables ### add '=' for the case when the user inputs "... where x=2, y=3" ### tokenize the user input and take out the stop words This function takes a list and flattens it if there are any other list elements in it      INPUTS     =======     l: any list      RETURNS     ========     list: flattened list           EXAMPLES     =========     >>> flatten(['2', '+', 'sin', '(', 'x', ')', '-', '3y', ['x', '2'], ['y', '3']])     >>> ['2', '+', 'sin', '(', 'x', ')', '-', '3y', 'x', '2', 'y', '3'] Cleans the string further. Gets rid of equal signs and some unneccessary punctuation.      INPUTS     =======     filtered_sentence: a list of tokenized parts of the user input      RETURNS     ========     list: a list of tokenized parts of the sentence but all cleaned      EXAMPLES     =========     >>> no_unwanted_symbols('x^2 + 2x - 3y when x= 1, y=3')     >>> ['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3'] ## get rid of comma ## take out the equal signs ## make sure there are no empty strings left in the list Inspects the tokenized list and finds the variables.      INPUTS     =======     filtered_sentence: a list of tokenized parts of the user input      RETURNS     ========     list: a list of variables     string: the unclean equation, essentially, the version of the first input without the stop words and all joined      EXAMPLES     =========     >>> get_variables(['x^2', '+', '2x', '-', '3y', 'x', '1', 'y', '3'])     >>> (['x', 'y'], 'x^2+2x-3yx1y3') Parses and separates the equation part from the values in the string      INPUTS     =======     filtered_sentence: a list of tokenized strings from the original equation     variables: list of the user defined variables     unclean_eq: essentially the version of the first input without stop the words      RETURNS     ========     string: the equation part     list: the variables and the values in the list      EXAMPLES     =========       >>> filtered_sentence = parse_sentence('x^2 + 2x when x is 1')     >>> get_eq_and_vals(filtered_sentence, ['x', 'y'], 'x^2+2xx1')     >>> ('x^2 + 2x', ['x', '1']) Returns a dictionary where the keys are the variable names and the values are the variable values      INPUTS     =======     variables: variables that are filtered via regex from the equation     vars_vals: the values filtered from the input      RETURNS     ========     dictionary: the keys are the variable names and the values are the variable values      EXAMPLES     =========     >>> variables = ['x']     >>> vars_vals = ['1']     >>> get_values(variables, vars_vals)     >>> {'x': '1'} This function takes a string and returns its single characters in the list      INPUTS     =======     word: any string      RETURNS     ========     list: a list of the characters of the word           EXAMPLES     =========     >>> split('word')     >>> ['w', 'o', 'r', 'd'] Takes in the equation part of the string and converts everything to python executable format      INPUTS     =======     eq: the equation part of the user defined string     variables: a list of variable names      RETURNS     ========     string: pythonic equation      EXAMPLES     =========     >>> pythonize('2^y + sin(7x)', ['x', 'y'])     >>> '2**y + sin(7*x)' ### take care of the exponents ### take care of division, power ### take care of the multiplication (ex: xy, 7y, 6xz) This function takes care of the whole parsing pipeline      INPUTS     =======     user_input: a string which typically includes some form of equation, variables and values      RETURNS     ========     string: final clean pythonic equation     dictionary: the keys are the variable names and the values are the variable values      EXAMPLES     =========     >>> pipeline('x^2 + y- 7xy where x is 2 and y is 8')     >>> ('x**2+y-7*x*y', {'x': '2', 'y': '8'}) ### tokenize and parse the sentence #get the variables and the unclean equation #clean the equation and get the values for the variables ### gets the variables and values in the dictionary format ### clean and pythonize the equation