Subroutine :: Approximate Ratio

A subroutine with a couple of examples:

ApproximateRatio-Subroutine.kmmacros (21,1 Ko)

To convert a floating point number to an approximate ratio, e.g.

`.285714 at precision 0.01 -> 2/7`

or to reduce a fraction to a simpler form, at a given level of precision, e.g.

`16 / 64 -> 1/4`

or to obtain a rough width:height pixel ratio for a image selected in the Finder:

The precision supplied (the granularity of the approximation) should be above 0 and <= 0.1

Expand disclosure triangle to view JS source
``````return (() => {
"use strict";

// main :: IO ()
const main = () => {
const
[precision, numerator, denominator] = [
kmvar.local_Precision,
kmvar.local_Numerator,
kmvar.local_Denominator
]
.map(s => new Function(`return \${s}`)());

return either(
)(
JSON.stringify
)(
bindLR(
"number" === typeof precision && (
0 < precision && 0.1 >= precision
)
? Right(precision)
: Left(
[
"Expected fractional precision <= 0.1",
`Saw: '\${kmvar.local_Precision}'`
]
.join("\n")
)
)(
nPrecision => bindLR(
checkedNumberLR("numerator")(numerator)
)(
nNumerator => bindLR(
checkedNumberLR("denominator")(
denominator
)
)(
nDenominator => ![0, Infinity]
.includes(nDenominator)
? Right(
approxRatio(nPrecision)(
nNumerator / nDenominator
)
)
: Left(
"Denominator should be non-zero and finite."
)
)
)
)
);
};

// checkedNumberLR :: String -> a -> Either String Number
const checkedNumberLR = valueName =>
v => isNaN(v)
? Left(
[
`Expected numeric value for \${valueName}`,
`saw: \${v}`
]
.join("\n")
)
: Right(v);

// ----------------------- JXA -----------------------

// alert :: String => String -> IO String
s => {
const sa = Object.assign(
Application("System Events"), {
});

return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ["OK"],
defaultButton: "OK"
}),
s
);
};

// --------------------- GENERIC ---------------------

// Ratio :: Integral a => a -> a -> Ratio a
const Ratio = a =>
b => {
const go = (x, y) =>
0 !== y
? (() => {
const d = gcd(x)(y);

return {
type: "Ratio",
// numerator
"n": Math.trunc(x / d),
// denominator
"d": Math.trunc(y / d)
};
})()
: undefined;

return go(a * signum(b), abs(b));
};

// Left :: a -> Either a b
const Left = x => ({
type: "Either",
Left: x
});

// Right :: b -> Either a b
const Right = x => ({
type: "Either",
Right: x
});

// abs :: Num -> Num
const abs = x =>
// Absolute value of a given number
// without the sign.
0 > x
? -x
: x;

// approxRatio :: Real -> Real -> Ratio
const approxRatio = epsilon =>
n => {
const
c = gcdApprox(
Boolean(epsilon)
? epsilon
: (1 / 10000)
)(1, n);

return Ratio(
Math.floor(n / c)
)(
Math.floor(1 / c)
);
};

// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = lr =>
// Bind operator for the Either option type.
// If lr has a Left value then lr unchanged,
// otherwise the function mf applied to the
// Right value in lr.
mf => "Left" in lr
? lr
: mf(lr.Right);

// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => "Left" in e
? fl(e.Left)
: fr(e.Right);

// gcd :: Integral a => a -> a -> a
const gcd = x =>
y => {
const zero = x.constructor(0);
const go = (a, b) =>
zero === b
? a
: go(b, a % b);

return go(abs(x), abs(y));
};

// gcdApprox :: Real -> (Real, Real) -> Real
const gcdApprox = epsilon =>
(x, y) => {
const _gcd = (a, b) => (
b < epsilon
? a
: _gcd(b, a % b)
);

return _gcd(Math.abs(x), Math.abs(y));
};

// signum :: Num -> Num
const signum = n =>
// Sign of a number.
n.constructor(
0 > n
? -1
: 0 < n
? 1
: 0
);

// showLog :: a -> IO ()
const showLog = (...args) =>
// eslint-disable-next-line no-console
console.log(
args
.map(JSON.stringify)
.join(" -> ")
);

return main();
})();
``````