to-inches
Version:
Convert metric to imperial measurements with fractions
318 lines (314 loc) • 10.7 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/to-inches.ts
var to_inches_exports = {};
__export(to_inches_exports, {
Inch: () => Inch,
default: () => to_inches_default,
toInches: () => toInches
});
module.exports = __toCommonJS(to_inches_exports);
// src/constants.ts
var Order = ["miles", "yards", "feet", "inches"];
var InputMultiplayer = {
mm: 1,
cm: 10,
m: 1e3,
km: 1e6,
in: 25.4,
ft: 304.8,
yd: 914.4,
mi: 1609344
};
var RawDividers = {
miles: 63360,
yards: 36,
feet: 12,
inches: 1
};
var DefaultOptions = {
denominator: 16,
input: "mm",
fractionClass: "fraction",
miles: true,
milesFraction: false,
milesTitle: "mi",
milesClass: "mi",
yards: true,
yardsFraction: false,
yardsTitle: "yd",
yardsClass: "yd",
feet: true,
feetFraction: false,
feetTitle: "ft",
feetClass: "ft",
inches: true,
inchesFraction: "fraction",
inchesTitle: "in",
inchesClass: "in",
templates: {
string: {
itemTemplate: "{{value}}{{fraction?{{value?\xA0:}}{{fraction}}:}}{{title?\xA0{{title}}:}}",
minus: "-",
joiner: " "
},
parts: {
itemTemplate: ["{{value}}", "{{fraction}}", "{{title}}"],
minus: "-"
},
html: {
itemTemplate: '<span{{class? class="{{class}}":}}>{{value}}{{fraction?{{value?\xA0:}}{{fraction}}:}}{{title?\xA0{{title}}:}}</span>',
fractionTemplate: `<span{{fractionClass? class="{{fractionClass}}":}}><sup>{{numerator}}</sup>⁄<sub>{{denominator}}</sub></span>`,
minus: "−",
joiner: " "
}
}
};
// src/to-inches.ts
var simpleRe = /{{(\w+)}}/g;
var conditionalRe = /{{(\w+)(?:\?((?:\\:|[^:{])*?):([^{]*?))?}}/gm;
var Inch = class _Inch {
#options = { ...DefaultOptions };
#sign;
#mm;
#calculated;
minus;
miles;
yards;
feet;
inches;
fraction;
constructor(mm, options) {
this.#options = {
...this.#options,
...options,
templates: {
string: {
...this.#options.templates.string,
...options?.templates?.string
},
parts: {
...this.#options.templates.parts,
...options?.templates?.parts
},
html: {
...this.#options.templates.html,
...options?.templates?.html
}
}
};
mm ??= 0;
mm *= InputMultiplayer[this.#options.input];
this.minus = mm < 0;
this.#sign = this.minus ? "-" : "+";
this.#mm = mm;
this.#calculated = this.#calculate(mm);
this.fraction = this.#calculated.fraction;
Object.assign(this, this.#calculated.accurate);
}
/**
* Rounds a number to a specified precision.
* If precision greater than 1 or less than 0, it will be treated as a number of decimal places.
* If precision is between 0 and 1, the decimals will be treated as a multiplier.
* Pay attention: to round to 1/4, you should pass 0.4, not 0.25.
* If precision is 0, the number will be rounded down.
* If precision is not specified, the number will be rounded.
* Negative precision can help with rounding integers. For example: round(123456789, -3) returns 123457000.
*/
static round(value, precision) {
if (precision === 0) return Math.floor(value);
if (!precision) return Math.round(value);
const multiplier = precision >= 1 || precision < 0 ? 10 ** precision : Number(String(Math.abs(precision)).slice(2));
return Math.round(value * multiplier) / multiplier;
}
/**
* Returns the greatest common divisor of two numbers.
*/
static gcd(a, b) {
return b ? _Inch.gcd(b, a % b) : a;
}
#calculate(mm) {
let raw = Math.abs(mm) / 25.4;
const sizes = this.#getSizes();
const precise = { miles: mm / 1609344, yards: mm / 914.4, feet: mm / 304.8, inches: mm / 25.4 };
const accurate = { miles: 0, yards: 0, feet: 0, inches: 0 };
const reminders = { miles: 0, yards: 0, feet: 0, inches: 0 };
for (const { key, value: size } of sizes) {
if (raw === 0) break;
const value = raw / size;
const fractionOption = this.#options[`${key}Fraction`];
const int = !fractionOption ? Math.floor(value) : fractionOption === true ? value : _Inch.round(value, fractionOption === "fraction" ? 0 : fractionOption);
accurate[key] = int;
reminders[key] = value - int;
raw -= int * size;
if (raw < 1e-15) raw = 0;
}
let fraction = "";
const minimal = this.#getMinimal(sizes);
if (this.#options[minimal.key] && minimal.fraction === "fraction") fraction = this.#calculateFraction(reminders[minimal.key]);
sizes.forEach(({ key }, index) => {
sizes[index].value = accurate[key];
});
return { precise, accurate, reminders, fraction, sizes };
}
#getSizes() {
const sizes = [];
let hasSense = true;
const order = [...Order];
while (hasSense && order.length) {
const key = order.shift();
if (this.#options[key]) sizes.push({ key, value: RawDividers[key] });
if (this.#options[key + "Fraction"]) hasSense = false;
}
return sizes;
}
#getInfo(key, sizes = this.#calculated.sizes) {
const index = sizes.findIndex((info) => info.key === key);
if (index < 0) return null;
const isLast = sizes.length - 1 === index;
const fraction = this.#options[`${key}Fraction`];
return {
key,
index,
isLast,
hasFraction: Boolean(fraction && isLast && this.fraction),
value: this[key],
title: this.#options[`${key}Title`],
class: this.#options[`${key}Class`],
fraction
};
}
#getMinimal(sizes = this.#calculated.sizes) {
const key = sizes.at(-1)?.key ?? Order.at(-1);
const info = this.#getInfo(key, sizes);
if (info === null) throw new Error("No minimal value found");
return info;
}
#calculateFraction(fractionPart) {
const denominator = this.#options.denominator;
let numerator = Math.round(fractionPart * denominator);
const commonDivisor = _Inch.gcd(numerator, denominator);
numerator /= commonDivisor;
const reducedDenominator = denominator / commonDivisor;
if (numerator === 0) return "";
if (numerator === reducedDenominator) return "1";
return `${numerator}/${reducedDenominator}`;
}
get mm() {
return this.#mm;
}
get precise() {
return this.#calculated.precise;
}
get reminders() {
return this.#calculated.reminders;
}
#processTemplate(template, context) {
do {
template = template.replace(simpleRe, (_, key) => String(context[key] ?? "")).replace(conditionalRe, (_, key, truthy, falsy) => context[key] ? truthy : falsy);
} while (simpleRe.test(template) || conditionalRe.test(template));
return template;
}
#render(info, itemTemplate, fractionTemplate) {
const isArray = Array.isArray(itemTemplate);
if (!info) return isArray ? [] : "";
if (!info.value && !(info.hasFraction && this.fraction) && !info.required) return isArray ? [] : "";
let fraction = "";
const [numerator, denominator] = this.fraction.split("/");
let context = {
minus: this.minus ? "\u2212" : "",
value: info.required ? info.value : info.value || "",
title: info.title ?? "",
class: info.class ?? "",
fractionClass: this.#options.fractionClass ?? "",
numerator,
denominator,
fraction
};
if (info.hasFraction) {
context.fraction = fractionTemplate ? this.#processTemplate(fractionTemplate, { ...context, numerator, denominator, fraction: this.fraction }) : this.fraction;
}
return isArray ? itemTemplate.map((template) => this.#processTemplate(template, context)) : this.#processTemplate(itemTemplate, context);
}
#renderSizes(type) {
const { itemTemplate, fractionTemplate, minus, joiner } = this.#options.templates[type];
let values = this.#calculated.sizes.map(({ key }) => this.#render(this.#getInfo(key), itemTemplate, fractionTemplate)).filter(Boolean).join(joiner ?? " ");
if (!values) {
values = this.#render({ ...this.#getMinimal(), required: true }, itemTemplate, fractionTemplate);
}
return (this.minus ? minus ?? "-" : "") + values;
}
toString() {
return this.#renderSizes("string");
}
toJSON() {
return {
minus: this.minus,
miles: this.miles,
yards: this.yards,
feet: this.feet,
inches: this.inches,
fraction: this.fraction
};
}
parts() {
const { itemTemplate, minus } = this.#options.templates.parts;
const clean = (array) => array.filter(Boolean).map((value) => isNaN(Number(value)) ? value : Number(value));
const result = [];
if (this.minus) result.push(minus ?? "-");
this.#calculated.sizes.forEach(({ key }) => {
const values = clean(this.#render(this.#getInfo(key), itemTemplate));
result.push(...values);
});
if (!result.length) {
const values = clean(this.#render({ ...this.#getMinimal(), required: true }, itemTemplate));
result.push(...values);
}
return result;
}
items() {
const items = this.#calculated.sizes.map(({ key }) => this.#getInfo(key)).filter((info) => info != null).filter((info) => info.value || info.hasFraction && this.fraction).map((info) => {
const data = { type: info.title ?? info.key, value: info.value };
if (info.hasFraction) data.fraction = this.fraction;
return data;
});
if (!items.length) {
const minimal = this.#getMinimal();
items.unshift({ type: minimal.title ?? minimal.key, value: 0 });
}
if (this.minus) items.unshift({ type: "sign", value: "-" });
return items;
}
html() {
return this.#renderSizes("html");
}
};
function toInches(mm, options) {
if (typeof mm === "object") {
const options2 = mm;
return { format: (mm2) => new Inch(mm2, options2) };
}
return new Inch(mm, options);
}
var to_inches_default = toInches;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Inch,
toInches
});