furud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/lib/furud/engine.rb +756 -0
- data/lib/furud/format.rb +244 -0
- data/lib/furud/formula.rb +538 -0
- data/lib/furud/functions.rb +794 -0
- data/lib/furud/types.rb +70 -0
- data/lib/furud/version.rb +5 -0
- data/lib/furud.rb +15 -0
- data/sig/furud.rbs +128 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 52a37960d9d35041b53670f21ac7f78e2bd84ea869cf93e20f75f7b26d68d83e
|
|
4
|
+
data.tar.gz: 684fc6a7fef9e252923ca7e477a0625789429e7fc4e807dce4f63073eed99820
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 67e93643cdbc0345a6e7d23b01eb91831a04bbd44def5619d3e0d0f292783906e8c9de95eabd050e90f0d5c817601f7070225c08a739d45c47dbc54bb47c3e52
|
|
7
|
+
data.tar.gz: 41ab1f4c958eb4f4472b21ab4570a1e55c9deb0d0805bce36c0e24d908ac7abfb511f478be533a9e58fc2a94c4d44963daa801aef465441c1bf8de8cf22f6f8a
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ydah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<h1 align="center">Furud</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>A dependency-free Ruby spreadsheet formula and recalculation engine</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/furud"><img src="https://img.shields.io/gem/v/furud.svg?colorB=319e8c" alt="Gem version"></a>
|
|
9
|
+
<a href="https://github.com/noxdea/furud/actions/workflows/main.yml"><img src="https://github.com/noxdea/furud/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg" alt="Ruby 3.2 or newer">
|
|
11
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
Furud parses and evaluates spreadsheet formulas without owning cell storage or a UI. It maintains cell dependencies, recalculates only affected formulas, reports cycles, translates A1/R1C1 references, spills array results, and formats numbers and dates. Runtime code uses Ruby's standard library only.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "furud"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Furud requires Ruby 3.2 or newer.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "furud"
|
|
28
|
+
|
|
29
|
+
source = {
|
|
30
|
+
["Sales", 1, 1] => 12,
|
|
31
|
+
["Sales", 2, 1] => 30
|
|
32
|
+
}
|
|
33
|
+
engine = Furud::Engine.new(source)
|
|
34
|
+
total = Furud::Reference.new(sheet: "Sales", row: 1, column: 2)
|
|
35
|
+
|
|
36
|
+
engine.set(total, "=SUM(A1:A2)")
|
|
37
|
+
engine.value(total) # => 42
|
|
38
|
+
engine.formula(total) # => "=SUM(A1:A2)"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rows and columns are one-based. A source is any object with `value_at(reference)` and optionally `each_in(area) { |reference, value| }`; a storage library can therefore provide sparse range iteration without becoming a Furud dependency. A plain Hash keyed by `[sheet, row, column]` or `Reference` also works.
|
|
42
|
+
|
|
43
|
+
`set` accepts a formula string beginning with `=` or a raw Ruby value. `value` recalculates pending work before returning a value. `recalculate` returns cells whose values changed; `dirty`, `precedents`, and `dependents` expose pending work and dependency traces. `clear` removes a cell. `define_name` accepts a `Reference` or `Area`.
|
|
44
|
+
|
|
45
|
+
## Formulas and references
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
origin = Furud::Reference.new(sheet: "Sales", row: 5, column: 2)
|
|
49
|
+
ast = Furud::Formula.parse("=SUM('Annual Plan'!$A$1:B2)+R[1]C[-1]", origin: origin)
|
|
50
|
+
Furud::Formula.render(ast, origin: origin) # => "=SUM('Annual Plan'!$A$1:B2)+A6"
|
|
51
|
+
Furud::Formula.references(ast) # => [an Area, a Reference]
|
|
52
|
+
|
|
53
|
+
copied = Furud::Formula.translate(ast, from: origin, to: Furud::Reference.new(sheet: "Sales", row: 5, column: 3))
|
|
54
|
+
adjusted = Furud::Formula.adjust(ast, type: :insert_rows, sheet: "Sales", at: 2, count: 1)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The parser supports A1 and R1C1 cell references, absolute and relative markers, quoted sheet names, cell ranges, named ranges, arrays, Excel-style operator precedence, comparison and concatenation operators, errors, unary signs, percentages, and function calls. Unqualified references resolve against the formula's origin sheet. Parse failures raise `Furud::ParseError`; they are not silently evaluated as partial formulas.
|
|
58
|
+
|
|
59
|
+
`Engine#insert_rows`, `#delete_rows`, `#insert_columns`, and `#delete_columns` adjust formulas and move cells stored by the engine. The host remains responsible for applying the same structural edit to its own source storage.
|
|
60
|
+
|
|
61
|
+
## Calculation and functions
|
|
62
|
+
|
|
63
|
+
The default registry contains 192 functions across arithmetic, statistics, logic, text, dates, lookup/information, and basic finance. It is extensible:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
functions = Furud::Functions.standard
|
|
67
|
+
functions.register("DOUBLE", arity: 1) { |number| number * 2 }
|
|
68
|
+
engine = Furud::Engine.new(source, functions: functions)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The function count describes registered names, not complete Excel compatibility. The covered standard-function cases have explicit expected values, but this is not a claim of full Excel parity. `XLOOKUP` supports exact, forward searches over one-dimensional horizontal or vertical lookup arrays, including corresponding row/column results; approximate and wildcard matching, reverse search, and binary search are not implemented and return `#VALUE!`. `INDIRECT` resolves A1 references in `Engine` formulas, but not R1C1 references. `OFFSET` also requires `Engine` formula context. `ISFORMULA` requires `Engine` reference context; calling it directly through the function registry returns `false`. `CELL` is a deliberate v1 limitation and returns `#VALUE!` because workbook/file metadata is not available to the formula engine. Financial solvers use bounded numeric iteration, so difficult or multiple-root cases may not converge to Excel's chosen result; formula coercion and advanced statistical/date edge cases are also not exhaustive.
|
|
72
|
+
|
|
73
|
+
Formula errors are values: `#DIV/0!`, `#VALUE!`, `#REF!`, `#NAME?`, `#N/A`, `#NUM!`, `#CYCLE!`, `#CALC!`, and `#SPILL!` for occupied array spill areas. Errors propagate through calculations; `IF`, `IFERROR`, `IFNA`, and `FILTER` handle only the errors relevant to their selected result. `iterative: true` enables bounded fixed-point evaluation for circular formulas.
|
|
74
|
+
|
|
75
|
+
Array constants and dynamic-array functions such as `SEQUENCE`, `TRANSPOSE`, and `UNIQUE` return a top-left value and spill into adjacent empty cells. Furud never overwrites non-empty cells; a blocked spill returns `#SPILL!`.
|
|
76
|
+
|
|
77
|
+
Array support is an MVP subset: rectangular constants, scalar broadcasting, and selected functions (`SEQUENCE`, `SORT`, `TRANSPOSE`, `UNIQUE`, `FILTER`) are supported. `FILTER` accepts row or column masks and an optional empty-result fallback. `UNIQUE` compares rows by default or columns with `by_col`, and `exactly_once` keeps only rows or columns that occur once; its mode arguments accept logical values or `0`/`1`, and an empty result is `#CALC!`. Full spreadsheet dynamic-array semantics are not.
|
|
78
|
+
|
|
79
|
+
## Number formats
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
spec = Furud::Format.parse("#,##0.00;[Red](#,##0.00)")
|
|
83
|
+
Furud::Format.apply(-1234.5, spec) # => ["(1,234.50)", { section: 1, color: :red }]
|
|
84
|
+
Furud::Format.apply(Date.new(2026, 9, 23), Furud::Format.parse("yyyy/mm/dd"))
|
|
85
|
+
# => ["2026/09/23", { section: 0 }]
|
|
86
|
+
|
|
87
|
+
Furud::Format.infer("12,5", locale: :de) # => [12.5, "0.00"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Common numeric/date tokens, up to four sections, positive/negative/zero/text selection, simple numeric conditions, named colors, decimal/grouping locale marks, percentages, and quoted literals are supported. This is a formatting engine for spreadsheet display, not an XLSX compatibility layer; unsupported Excel format directives are preserved as literal text where possible.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bundle install
|
|
96
|
+
bundle exec rake
|
|
97
|
+
bundle exec rbs -I sig validate
|
|
98
|
+
bundle exec rake bench
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`BUDGET=1 bundle exec rake bench` checks the 100,000-formula workload against the 10 ms incremental and 3 second full-recalculation budgets. See [architecture decisions](docs/adr/README.md).
|