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:

(See Show dimensions of selected image file in Finder? - Questions & Suggestions - Keyboard Maestro Discourse)

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();
})();