Rational Approximation
The SheetJS frac
library computes rational approximations to floating point
numbers with bounded denominator. It is a core component in number formatting,
powering "Fraction with up to 1 digit" and related number formats.
The library is also available for standalone use on the SheetJS CDN1.
Source code and project documentation are hosted on the SheetJS Git server at https://git.sheetjs.com/sheetjs/frac
Live Demo
Formatted texts are calculated from the value and maximum denominators.
Please report an issue if a particular result does not align with expectations.
function SheetJSFrac() { const [val, setVal] = React.useState(0.699450515); const [text, setText] = React.useState(""); if(typeof frac == "undefined") return ( <b>ERROR: Reload this page</b> ); const fmt = arr => `${(""+arr[1]).padStart(5)} / ${(""+arr[2]).padEnd(5)}`; React.useEffect(() => { if(typeof frac == "undefined") return setText("ERROR: Reload this page!"); let v = +val; if(!isFinite(v)) return setText(`ERROR: ${val} is not a valid number!`); try { fmt(frac(val, 9)); setText(""); } catch(e) { setText("ERROR: " + (e && e.message || e)); } }, [val]); const n = { textAlign:"right" }; const g = { backgroundColor:"#C6EFCE", color:"#006100", whiteSpace:"pre-wrap" }; const b = { backgroundColor:"#FFC7CE", color:"#9C0006" }; return ( <table> <tr><td><b>Number Value</b></td><td colspan="4"> <input type="text" value={val} onChange={e => setVal(e.target.value)}/> </td></tr> <tr><td></td><th>Max Denom</th><th>Mediant</th><th>Continued Frac</th></tr> {[1,2,3,4,5].map(d => ( <tr> <td><b>Up to {d} Digit{d == 1 ? "" : "s"}</b></td> <td style={n}><code>{10**d - 1}</code></td> <td><code style={text?b:g}>{text||fmt(frac(val,10**d-1))}</code></td> <td><code style={text?b:g}>{text||fmt(frac.cont(val,10**d-1))}</code></td> </tr> ))} </table> ); }
API
In the browser, the library exports the frac
global. In NodeJS, the library
default export is a function.
Algorithms
The "Mediant" algorithm (frac
in the browser; the default export in NodeJS)
calculates the exact solution.
The "Continued Fractions" algorithm (frac.cont
in the browser; the cont
field in the NodeJS export) calculates an approximate solution but has better
worst-case runtime performance.
Spreadsheet software use these algorithms to render number formats including
?/?
and ??/??
. The algorithm choices are summarized in the following table:
Spreadsheet Software | Algorithm |
---|---|
SheetJS | Continued Fractions |
Apple Numbers | Mediant Algorithm |
Google Sheets | Mediant Algorithm |
Lotus 1-2-3 | (unsupported) |
Microsoft Excel | Continued Fractions |
Quattro Pro | Continued Fractions |
WPS 电子表格 | Mediant Algorithm |
There are known rounding errors in LibreOffice2 which result in inaccurate fraction calculations.
The LibreOffice developers believe these numerical errors are desirable:
"We ignore the last two bits for many stuff to improve the user experience."
It is strongly recommended to use a different spreadsheet tool for accurate data processing involving fractions and numeric data.
Functions
Both functions accept three arguments:
var frac_mediant = frac(value, denominator, mixed);
var frac_cont = frac.cont(value, denominator, mixed);
value
: original valueD
: maximum denominator (e.g. 99 = "Up to 2 digits")mixed
: iftrue
, return a mixed fraction.
The return value is an array with three integers:
var [ int, num, den ] = result;
int
(first element) represents the integer part of the estimate.num
(second element) is the numerator of the fractionden
(second element) is the positive denominator of the fraction
The estimate can be recovered from the array:
var estimate = int + num / den;
If mixed
is false
, then int = 0
and 0
< den
≤ D
If mixed
is true
, then 0
≤ num
< den
≤ D
When mixed
is true, int
will be the floor of the result. For example, in
var result = frac( -0.125 , 9, true);
the result will be [ -1, 7, 8 ]
. This is interpreted as
-0.125 ~ (-1) + (7/8)
Footnotes
-
See https://cdn.sheetjs.com/frac/ for more details. ↩
-
See issue #83511 in the LibreOffice bug tracker. ↩