131 / Advanced

Safe user formulas for payroll

A payroll lets the user define concepts with a formula — salario_diario * dias * 1.5, min(base, 5 * salario_minimo) * 4 / 100. std/expr is an expression evaluator in pure Nyx with no access to anything beyond the variables you hand it, which is what makes it safe to run text a user typed.

131-expr-formulas-nomina.nxSource →
// Nyx by Example: fórmulas de usuario seguras (nómina) con std/expr.
//
// El caso real: una nómina deja que el usuario defina conceptos con una fórmula
// (`salario_diario * dias * 1.5`, `min(base, 5 * salario_minimo) * 4 / 100`).
// std/expr es un evaluador de expresiones en Nyx puro, sin acceso a nada fuera de
// las variables que se le dan.
//
// Lo que muestra:
//   - expr_parse compila UNA vez (y valida al guardar la fórmula: el Err trae la
//     posición del error); expr_eval_dec la evalúa por cada empleado;
//   - expr_vars dice qué variables usa la fórmula, para verificar que existan;
//   - PRECISIÓN: es plata, así que los valores son decimales de punto fijo con 6
//     decimales, no float: 0.1 + 0.2 da exactamente 0.3 y round(2.675, 2) da 2.68
//     (en float da 0.30000000000000004 y 2.67);
//   - funciones permitidas: min, max, round, floor, ceil, abs, if(cond, a, b);
//   - los errores de evaluación (variable sin definir, división por cero) también
//     dicen la posición.

import "std/expr"
import "std/error"

fn evaluar(texto: String, v: ExprVars, decimales: int) {
    match expr_parse(texto) {
        Result.Ok(f) => {
            match expr_eval_dec(f, v, decimales) {
                Result.Ok(s) => { println(texto + "  =>  " + s) }
                Result.Err(e) => { println(texto + "  =>  " + e.msg) }
            }
        }
        Result.Err(e) => { println(texto + "  =>  " + e.msg) }
    }
}

fn main() {
    // Datos de un empleado. set_dec recibe el decimal EXACTO en texto.
    var v: ExprVars = expr_vars_new()
    v.set_dec("salario_diario", "43.33")
    v.set_int("dias", 15)
    v.set_dec("base", "12345.67")
    v.set_dec("salario_minimo", "248.93")

    evaluar("salario_diario * dias * 1.5", v, 2)
    evaluar("min(base, 5 * salario_minimo) * 4 / 100", v, 2)
    evaluar("if(dias >= 15, salario_diario * dias * 0.05, 0)", v, 2)

    // Por qué no float.
    evaluar("0.1 + 0.2", v, 2)
    evaluar("0.1 + 0.2 == 0.3", v, 0)
    evaluar("round(2.675, 2)", v, 2)

    // Validar al guardar la fórmula: qué variables usa y si el catálogo las tiene.
    match expr_parse("min(base, 5 * salario_minimo) * porcentaje / 100") {
        Result.Ok(f) => {
            let usadas: Array = expr_vars(f)
            var i: int = 0
            while i < usadas.length() {
                let nombre: String = usadas[i]
                if v.has(nombre) {
                    println("variable " + nombre + ": ok")
                } else {
                    println("variable " + nombre + ": NO existe en el catálogo")
                }
                i = i + 1
            }
        }
        Result.Err(e) => { println(e.msg) }
    }

    // Errores con posición.
    evaluar("base / (dias - 15)", v, 2)
    evaluar("salario * 2", v, 2)
    evaluar("2 + * 3", v, 2)
}
Outputstdout
salario_diario * dias * 1.5  =>  974.93
min(base, 5 * salario_minimo) * 4 / 100  =>  49.79
if(dias >= 15, salario_diario * dias * 0.05, 0)  =>  32.50
0.1 + 0.2  =>  0.30
0.1 + 0.2 == 0.3  =>  1
round(2.675, 2)  =>  2.68
variable base: ok
variable salario_minimo: ok
variable porcentaje: NO existe en el catálogo
base / (dias - 15)  =>  expr: división por cero (posición 6)
salario * 2  =>  expr: variable sin definir 'salario' (posición 1)
2 + * 3  =>  expr: se esperaba un valor y hay '*' (posición 5)

How it works

expr_parse compiles a formula ONCE — and validates it when it is saved: the Err carries the position of the mistake — and expr_eval_dec evaluates it for each employee. expr_vars says which variables a formula uses, so you can check they exist before running it (the variable porcentaje: NO existe line).

Precision matters because this is money: values are fixed-point decimals with 6 places, not float. 0.1 + 0.2 is exactly 0.3 and round(2.675, 2) is 2.68, where a float gives 0.30000000000000004 and 2.67. The allowed functions are min, max, round, floor, ceil, abs and if(cond, a, b). Evaluation errors — an undefined variable, a division by zero — also say where, as in the last three lines.