Skip to main content

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.

Result
Loading...
Live Editor
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 SoftwareAlgorithm
SheetJSContinued Fractions
Apple NumbersMediant Algorithm
Google SheetsMediant Algorithm
Lotus 1-2-3(unsupported)
Microsoft ExcelContinued Fractions
Quattro ProContinued Fractions
WPS 电子表格Mediant Algorithm
LibreOffice bugs

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 value
  • D: maximum denominator (e.g. 99 = "Up to 2 digits")
  • mixed: if true, 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 fraction
  • den (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 < denD

If mixed is true, then 0num < denD

Negative Values

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

  1. See https://cdn.sheetjs.com/frac/ for more details.

  2. See issue #83511 in the LibreOffice bug tracker.