formulas

repository·master·Indexed 19 days ago

https://github.com/vinci1it2000/formulas

An Excel formula interpreter for Python that parses, compiles, and executes Excel expressions. It allows for the compilation of entire workbooks into Python code via ExcelModel, enabling execution without the Excel COM server or Excel itself. The library includes a CLI for calculating workbooks, building JSON model representations, and testing spreadsheet accuracy, as well as a Flask-based API and GUI for serving models over HTTP.

Tokens
6.5K
Snippets
22
Records
27
Agent score
67%

What's inside formulas

  1. Install formulas with all extras

    master

    To enable full functionality, including Excel workbook compilation (ExcelModel) and plotting capabilities, install the all extra.

    • excel: Enables compiling and executing Excel workbooks via formulas.excel.ExcelModel.
    • plot: Enables plotting the formula AST and the Excel model.
    $ pip install formulas[all]
  2. Run batch calculations via the CLI

    master

    For automated workflows, you can use the formulas.cli module to run multiple calculation scenarios in a single batch.

    1. Create a JSON batch file where each entry contains a name, an overwrite dictionary for inputs, and a renders list for outputs.
    2. Execute the CLI using python -m formulas.cli calc <workbook_path> --batch <batch_file_path>.

    Available CLI flags for batching:

    • --batch: Path to the JSON batch file.
    • --processes: Number of processes to use for parallel execution.
    • --output-format: Set to json to receive a summary of results.
    • --output-dir: Directory where results will be saved.
    import json
    import subprocess
    import sys
    import tempfile
    from pathlib import Path
    
    with tempfile.TemporaryDirectory() as tmp:
        batch = Path(tmp) / 'batch.json'
        batch.write_text(json.dumps([
            {
                'name': 'base',
                'overwrite': {
                    "'[excel.xlsx]'!INPUT_A": 3,
                    "'[excel.xlsx]DATA'!B3": 1,
                },
                'renders': ["'[excel.xlsx]DATA'!C2=result"],
            },
            {
                'name': 'stress',
                'overwrite': {
                    "'[excel.xlsx]'!INPUT_A": 4,
                    "'[excel.xlsx]DATA'!B3": 1,
                },
                'renders': ["'[excel.xlsx]DATA'!C2=result"],
            },
        ], indent=2))
    
        result = subprocess.run([
            sys.executable, '-m', 'formulas.cli', 'calc',
            'test/test_files/excel.xlsx',
            '--batch', str(batch),
            '--processes', '2',
            '--output-format', 'json',
            '--output-dir', tmp,
        ], capture_output=True, text=True, check=False)
    
        assert result.returncode == 0, result.stderr
        summary = json.loads(result.stdout)
        assert [item['name'] for item in summary] == ['base', 'stress']
  3. How to format scalar values in CLI commands

    master

    When using --overwrite in test or calc, values must follow these formats:

    • Strings: Must be enclosed in double quotes, e.g., A1="my string".
    • Booleans: Use TRUE or FALSE (case-insensitive).
    • Dates: Must use YYYY-MM-DD format, e.g., A1=2024-12-31.
    • Numbers: Standard integer or float notation (e.g., 10, 3.14).
  4. Load, calculate, and write an Excel workbook

    master

    Use formulas.ExcelModel() to handle entire Excel files.

    1. Load: Use .loads("path/to/file.xlsx") followed by .finish() to prepare the model.
    2. Circular References: If your workbook contains circular references, you must pass circular=True to the .finish() method.
    3. Calculate: Call .calculate() to compute cell values.
    4. Write: Call .write() to save the calculated results back to a file.
    5. Dependency Graph: Access the dependency graph via xl_model.dsp and use .plot(view=False) to visualize cell relationships.
    import formulas
    
    # Load and finish the model
    xl_model = formulas.ExcelModel().loads("../test/test_files/excel.xlsx").finish()
    
    # Calculate and write results
    xl_model.calculate()
    xl_model.write()
    
    # Plot dependency graph
    dsp = xl_model.dsp
    dsp.plot(view=False)
  5. Integrate formulas with a Flask application

    master

    You can embed formulas as a calculation engine within a lightweight web application using formulas.app.create_app. This allows you to load a workbook once and expose its calculation logic via a JSON API.

    When using the API, you send a POST request to /api/calculate with a JSON body containing:

    • inputs: A dictionary mapping cell addresses (e.g., '[filename.xlsx]'!CELL') to their new values.
    • renders: A list of strings defining the output mapping in the format '[filename.xlsx]SHEET'!CELL=variable_name.
    from formulas.app import create_app
    
    app = create_app(files=('test/test_files/excel.xlsx',), circular=False)
    client = app.test_client()
    response = client.post('/api/calculate', json={
        'inputs': {
            "'[excel.xlsx]'!INPUT_A": 3,
            "'[excel.xlsx]DATA'!B3": 1,
        },
        'renders': ["'[excel.xlsx]DATA'!C2=result"],
    })
    
    assert response.status_code == 200
    assert response.get_json()['outputs'] == {'result': 10.0}
  6. Use ExcelModel as an ETL transformer

    master

    You can treat an Excel workbook as a transformation step in a data pipeline. By using ExcelModel, you can compile a specific subset of inputs and outputs into a function that can be called repeatedly with structured data.

    Workflow:

    1. Load the workbook using formulas.ExcelModel().loads(path).finish().
    2. Compile a function using .compile(inputs=[...], outputs=[...]).
    3. Call the resulting function with values. The function returns a tuple where the first element contains the calculated values (accessible via .value).
    import formulas
    
    model = formulas.ExcelModel().loads('test/test_files/excel.xlsx').finish()
    func = model.compile(
        inputs=["'[excel.xlsx]'!INPUT_A", "'[excel.xlsx]DATA'!B3"],
        outputs=["'[excel.xlsx]DATA'!C2"],
    )
    records = [
        {'id': 'row-1', 'input_a': 3, 'b3': 1},
        {'id': 'row-2', 'input_a': 4, 'b3': 1},
    ]
    results = []
    
    for record in records:
        result, = func(record['input_a'], record['b3'])
        results.append({
            'id': record['id'],
            'result': result.value[0, 0],
        })
    
    assert results == [
        {'id': 'row-1', 'result': 10.0},
        {'id': 'row-2', 'result': 11.0},
    ]
  7. Add custom functions to the parser

    master

    You can extend the formula interpreter by adding your own Python functions to the global function registry.

    import formulas
    
    # Get the current registry
    FUNCTIONS = formulas.get_functions()
    
    # Add a new function
    FUNCTIONS['MYFUNC'] = lambda x, y: 1 + y + x
    
    # Use it in a formula
    func = formulas.Parser().ast('=MYFUNC(1, 2)')[1].compile()
    print(func())  # Output: 4
    import formulas
    FUNCTIONS = formulas.get_functions()
    FUNCTIONS['MYFUNC'] = lambda x, y: 1 + y + x
    func = formulas.Parser().ast('=MYFUNC(1, 2)')[1].compile()
    print(func())
  8. Load and execute an Excel workbook with ExcelModel

    master

    Use formulas.ExcelModel to load, compile, and execute entire Excel workbooks without needing Excel installed.

    Basic Workflow

    import formulas
    
    # Load and finish the model
    xl_model = formulas.ExcelModel().loads('excel.xlsx').finish()
    
    # Calculate the model
    xl_model.calculate()
    
    # Write results to a directory
    xl_model.write(dirpath='output')

    Note: If your workbook contains circular references, pass circular=True to the .finish() method.

    Overriding inputs and defining outputs

    You can pass specific cell values as inputs and specify which cells should be returned as outputs in the calculate() method:

    xl_model.calculate(
        inputs={
            "'[excel.xlsx]'!INPUT_A": 3,  # Overwrite default
            "'[excel.xlsx]DATA'!B3": 1    # Impose value
        },
        outputs=[
            "'[excel.xlsx]DATA'!C2", 
            "'[excel.xlsx]DATA'!C4"
        ]
    )
    import formulas
    xl_model = formulas.ExcelModel().loads('excel.xlsx').finish()
    xl_model.calculate()
  9. Parse and execute an Excel formula

    master

    You can parse a raw Excel formula string into a compiled function using formulas.Parser().

    To see the required inputs, use the .inputs attribute. You can also visualize the formula using .plot().

    import formulas
    # Parse and compile the formula
    func = formulas.Parser().ast('=(1 + 1) + B3 / A2')[1].compile()
    
    # Inspect inputs
    print(list(func.inputs))  # ['A2', 'B3']
    
    # Execute with values
    result = func(1, 5)
    print(result)  # Array(7.0, dtype=object)
    
    # Visualize (set view=True to open in browser)
    func.plot(view=False)
    import formulas
    func = formulas.Parser().ast('=(1 + 1) + B3 / A2')[1].compile()
    func(1, 5)
  10. Load a partial Excel model from ranges

    master

    If you only need a subset of a workbook, use from_ranges to load a partial model based on specific output cells and ranges. This reduces the scope of the model to only what is necessary for those outputs.

    import formulas
    
    xl = formulas.ExcelModel().from_ranges(
        "'[example.xlsx]DATA'!C2:D2",  # Output range
        "'[example.xlsx]DATA'!B4"      # Output cell
    )
    xl = formulas.ExcelModel().from_ranges("'[example.xlsx]DATA'!C2:D2", "'[example.xlsx]DATA'!B4")
  11. Compile an ExcelModel into a Python function

    master

    You can transform an ExcelModel into a high-performance DispatchPipe_ object (a callable function) by defining specific input and output cell references. This is useful for treating a spreadsheet like a standard Python function.

    # Define inputs and outputs by cell reference
    func = xl_model.compile(
        inputs=[
            "'[excel.xlsx]'!INPUT_A",
            "'[excel.xlsx]DATA'!B3"
        ],
        outputs=[
            "'[excel.xlsx]DATA'!C2", 
            "'[excel.xlsx]DATA'!C4"
        ]
    )
    
    # Execute the function with positional arguments
    # The return value is a list of results corresponding to the outputs
    results = [v.value[0, 0] for v in func(3, 1)]
    print(results)  # e.g., [10.0, 4.0]
    func = xl_model.compile(
        inputs=["'[excel.xlsx]'!INPUT_A", "'[excel.xlsx]DATA'!B3"],
        outputs=["'[excel.xlsx]DATA'!C2", "'[excel.xlsx]DATA'!C4"]
    )
    results = [v.value[0, 0] for v in func(3, 1)]