r/ruby 3d ago

acts_as_calculator - An open-source mountable engine for complex formula calculation built upon Dentaku, with full versioning and auditing support

https://github.com/lautarograc/acts_as_calculator

in RubyGems

I built this gem based on my experience of implementing very similar (perhaps even identical) services in extremely different business domains. For example, I currently work in a Payroll SaaS, which calculates payslips using "concepts" (which are basically calculated formulas at a certain point in time, for example, a concept may be "health insurance discount", "performance bonus"). I supported a system like that when I worked at a delivery app, for supporting dynamic checkouts. I imagine you can use the same to calculate the premiums in a car insurance calculation, and a plethora of other domains.

The idea is simple; you mix the gem into your models

class Employee < ApplicationRecord
  include ActsAsCalculator::Calculable
end

Then, you have two classes, ActsAsCalculator::Formula + ActsAsCalculator::FormulaVersion , so each formula version is accesible from a slice window (effective_from | effective_to),

formula = ActsAsCalculator::Formula.create!(key: "monthly_taxes", scope: "payroll")
puts employee.salary
5000.0
ActsAsCalculator::PublishFormulaVersion.(
  formula: formula,
  expression: "salary * 0.22",
  effective_from: Date.new(2026, 1, 1),
  effective_to:   Date.new(2026, 6, 30),
  variables: [{ name: "salary", source_type: "attribute" }]
)

You can now use these formulas to discount a tax from the employee salary. Important to note, formulas are parsed and calculated through Dentaku, which long ago already solved safe logic eval.

employee = Employee.last

result = employee.calculate(
  :monthly_taxes,
  as_of: Date.new(2026, 3, 15)
)


puts result.inspect

#<data ActsAsCalculator::Result value=0.11e4, breakdown={:expression=>"salary * 0.22", :inputs=>{"salary"=>0.5e4}, :value=>0.11e4}, formula_version=#<ActsAsCalculator::FormulaVersion id: 17, formula_id: 10, version_number: 6, expression: "salary * 0.22", effective_from: "2026-01-01", effective_to: "2026-06-30", status: "active", change_note: nil, created_at: "2026-09-01 01:26:15.204120000 +0000", updated_at: "2026-09-01 01:26:15.204120000 +0000">, as_of=Sun, 15 Mar 2026>
puts result.value
1100.0

(ActsAsCalculator::Result is a value object)

Then, a key feature, you can also template the result using sandboxed Liquid (this templating is functional but not yet feature-complete, I have a small roadmap still planned for this)

template = ActsAsCalculator::Template.create!(
  key: "payslip", scope: "payroll", format: "text",
  body: "Gross: {{ result.value | currency }}\n }}"
)

employee.render(:payslip, calculate: :monthly_taxes, as_of: Date.today)

=> "Gross: 1,100.00 "

The engine also has support (experimental for now) for JSON imports, and exports are still on the roadmap.

The engine also supports exposing HTTP endpoints for managing formulas/templates, but this is disabled by default — you opt in via a config flag before mounting it.ActsAsCalculator.configure { |c| c.enable_api = true }

The gem has also a supporting frontend engine, acts_as_calculator_editor, built upon Hotwire and Lexxy.

A couple of notes:

the base gem acts_as_calculator is human-made, written from my experience of building from scratch an identical but way-less generic production-used implementation of this, so most of the architecture is already proven at scale. I used AI to add a couple helper methods, to do code review, to complete documentation and to write several unit tests. The supporting gem acts_as_calculator_editor was fully built with an AI agentic workflow.

I haven't yet published the 1.0.0 version since I still have a key feature to implement (chaineable formulas, so a formula is able to call other formulas directly).

The gem also exposes a lot of apportionment, distribution and allocation helper strategies (:proportional, :equal, :largest_remainder), which are one of the main motivations I had for creating this gem. In the v1 version, I will add comprehensive api docs for them.

The gem is also intended to be used with ActiveRecord, but should be framework agnostic for most things.

If you have any feedback to give about the gem and the idea, please leave a comment. This is my first experience building something for the OSS space that has given me so much, so feel free to contribute and criticize.

23 Upvotes

4 comments sorted by

1

u/sshaw_ 2d ago

Looks nice. Certainly something I've dealt with at multiple organizations. Some thoughts that may help drive adoption:

In the README I'd move Calculator::Formula.create! before Employee#calculate. I would also explain how :as_of applies to effective_* attributes. #calculate uses base_salary but the Formula looks like it uses Employee#salary.

More info on the supported Liquid filters and add some error handling examples.

ActsAsCalculator::FormulaVersion

Is this stored in the DB via acts_as_calculator:install?

1

u/Lautaengalia 2d ago

Thanks! The readme was outdated, yesterday I modified a lot of the design architecture (implemented a decree pattern) and the code examples in the readme were not modified. I just released a feature and fixed the readme.

I would also explain how :asof applies to effective* attributes.

Yeah I took it for granted, the idea is simple, every formula version represents a slice on time (effective_from DateTime, effective_to DateTime), and you then calculate a formula based on a point in time (calculate X formula as_of it's version in DateTime).

More info on the supported Liquid filters and add some error handling examples.

I still have a lot of fleshing out to do in the templating, it's a key feature of what I want to build so I already have a pretty good picture of what I want, it's the next thing in the roadmap for the v1. For now I've just implemented the skeleton, I'm taking my time with it since I've never dived too deep into liquid and I really don't want to outsource this to the AI, but yes, v0.3 is going to be Liquid and Templates, and I'm probably going to add a wiki with comprehensive documentation of what it can do.

Is this stored in the DB via acts_as_calculator:install?

Yes, the generstor creates the migrations.

1

u/sshaw_ 18h ago

Thanks for the additional information.

One thing that may help with the variable lookup outside of Liquid is this gem. Especially if you start to support vars with call chains, e.g., user.salary.to_d

1

u/Intelligent_Glass248 1d ago

Cool simple framework to keep up with changing tax laws. ⭐️ starred!