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(
alert("Subroutine :: Approximate Ratio")
)(
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
const alert = title =>
s => {
const sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
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();
})();