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 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-23
4
+
5
+ - Initial release.
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).