npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vetwo/units

v0.0.2

Published

Generic, domain-agnostic scientific unit & dimensional-analysis engine (Mass, Time, Energy, Currency, Count). No knowledge of nutrition, feed, or any other domain.

Readme

@vetwo/units

A small, dependency-free scientific unit & dimensional-analysis engine for TypeScript.

It represents every unit as a vector of exponents over five orthogonal base dimensions — Mass, Time, Energy, Currency, Count — and lets you multiply, divide, add, subtract, and convert Quantity values with full dimensional safety, so incompatible units can never silently combine.

@vetwo/units is completely domain-agnostic — it has no knowledge of nutrition, feed, chemistry, or finance. It is the foundation that domain packages (like @vetwo/nutrition-units) build on top of.

import { Quantity } from "@vetwo/units";

const cp = Quantity.of(12, "%");
const intake = Quantity.of(10, "kg/day");

intake.multiply(cp).to("g/day"); // Quantity(1200, "g/day")

One generic operation — Quantity.multiply() — correctly handles percent, ppm, mg/kg, Mcal/kg, IU/kg, and any other ratio/rate unit, because it does real dimensional analysis instead of special-casing each unit type.


Table of contents


Installation

npm install @vetwo/units
# or
bun add @vetwo/units

No runtime dependencies. Requires TypeScript ^5.x for full type support (plain JS consumers can still use it — types are just erased).


Core concept: dimensions

Every unit is described by a dimension vector — exponents over five base dimensions:

| Symbol | Dimension | Example units | |---|---|---| | M | Mass | kg, g, mg, µg, lb | | T | Time | day, hour | | E | Energy | Mcal, Kcal, MJ | | C | Currency | cur | | N | Count / biological activity | IU | | (empty vector) | Dimensionless ratio | %, ppm, fraction, and any compound that cancels out (e.g. mg/kg) |

Composite units are parsed generically as "<numerator>/<denominator>". The parser looks up each side as an atomic unit and combines their dimension vectors and conversion factors — so "Mcal/kg", "IU/day", "cur/kg", "mg/kg" all work automatically with zero special-casing. Crucially, if a compound's dimensions cancel out entirely (mass ÷ mass, as in mg/kg), the parser recognizes this and treats it as a plain ratio — identical in kind to % or ppm, just with a different scale factor.


Built-in units

Mass:      kg, g, mg, µg, lb
Time:      day, hour
Energy:    Mcal, Kcal, MJ
Currency:  cur          (generic — map to your real currency symbol upstream)
Count:     IU
Ratio:     fraction, %, ppm

Any "A/B" combination of the above (or of units you register yourself) works immediately without extra configuration.

Basis tags

A unit string may carry a trailing basis tag: "% DM", "Mcal/kg DM", "IU/kg asFed". This is stored as metadata (unit.basis) and does not participate in dimensional math — converting between reporting bases (e.g. dry-matter vs as-fed) needs an extra domain input (a moisture percentage) and is therefore left to domain packages, not this engine.


API reference

Quantity

The central value object of the whole package — an immutable { value, unit } pair with dimensionally-safe arithmetic. Every calculation in an application built on this package should flow through Quantity instances.

class Quantity {
  static of(value: number, unit: string | Unit, registry?: UnitRegistry): Quantity;
  to(unit: string | Unit, registry?: UnitRegistry): Quantity;
  toBase(): Quantity;
  add(other: Quantity): Quantity;
  subtract(other: Quantity): Quantity;
  multiply(other: Quantity): Quantity;
  divide(other: Quantity): Quantity;
  scale(factor: number): Quantity;
  equals(other: Quantity, epsilon?: number): boolean;
  hasSameDimension(other: Quantity): boolean;
  toString(): string;
  readonly value: number;
  readonly unit: Unit;
}

Quantity.of(value, unit)

Creates a new quantity. unit can be a string ("kg", "Mcal/day") or an already-resolved Unit object.

const weight = Quantity.of(450, "kg");
const rate = Quantity.of(2.1, "kg/day");

.to(unit)

Converts to another unit of the same dimension. Throws ImpossibleConversionError if the dimensions differ, or ConversionError if the two units have conflicting basis tags (e.g. "% DM""%" directly — use a domain BasisConverter for that instead).

Quantity.of(1, "kg").to("g");        // Quantity(1000, "g")
Quantity.of(3000, "Kcal").to("Mcal"); // Quantity(3, "Mcal")

.toBase()

Returns the value expressed in the canonical base unit of its dimension (kg for Mass, day for Time, Mcal for Energy, cur for Currency, IU for Count, fraction for Ratio). Useful for comparisons and validation.

Quantity.of(50, "%").toBase().value; // 0.5

.add(other) / .subtract(other)

Dimensionally-safe addition/subtraction. The result is expressed in the unit of the receiver (this). Throws UnitMismatchError if dimensions differ.

Quantity.of(1, "kg").add(Quantity.of(500, "g"));   // Quantity(1.5, "kg")
Quantity.of(1, "kg").add(Quantity.of(1, "Mcal"));  // throws UnitMismatchError

.multiply(other) / .divide(other)

Performs real dimensional analysis: the resulting dimension is the product (or quotient) of the two operands' dimensions, computed automatically. This is the operation that replaces hand-written formulas like cp/100 * kg or mg/kg * kg everywhere in a consuming application.

const cp = Quantity.of(12, "%");
const intake = Quantity.of(10, "kg/day");
intake.multiply(cp).to("g/day"); // Quantity(1200, "g/day")

const price = Quantity.of(0.35, "cur/kg");
intake.multiply(price).to("cur/day"); // Quantity(3.5, "cur/day")

.scale(factor)

Multiplies the numeric value by a plain number, keeping the same unit. Use this only for dimensionless scaling factors (e.g. a safety margin), not for anything that has its own unit — use .multiply() for that.

Quantity.of(10, "kg/day").scale(1.1); // Quantity(11, "kg/day") — a 10% buffer

.equals(other, epsilon?)

Compares two quantities for equality within a tolerance, after normalizing both to their base unit. Returns false (not an error) if dimensions differ.

Quantity.of(1000, "g").equals(Quantity.of(1, "kg")); // true

.hasSameDimension(other)

Checks dimension compatibility without throwing — the safe way to guard a conversion or comparison before doing it.

if (value.hasSameDimension(requirement)) { /* safe to compare */ }

.toString()

Human-readable debug string, e.g. "12 % DM", "3.2 Mcal/kg".


Q() factory

Sugar for Quantity.of().

import { Q } from "@vetwo/units";
const intake = Q(10, "kg/day");

parseUnit()

Parses a unit string into a resolved Unit object without creating a Quantity. Mostly useful internally or when you need to inspect a unit's dimension before deciding what to do with a value.

import { parseUnit } from "@vetwo/units";

const unit = parseUnit("mg/kg DM");
unit.dimension; // {} (dimensionless — mass/mass cancels)
unit.basis;     // "DM"

UnitRegistry

Holds the set of known atomic units. The package ships a defaultUnitRegistry seeded with the built-in units; you can register more atomic units for your own domain, or create an isolated registry instance (useful in tests or multi-tenant setups).

class UnitRegistry {
  constructor(seed?: readonly AtomicUnitDef[]);
  registerAtomic(def: AtomicUnitDef): void;
  hasAtomic(symbol: string): boolean;
  getAtomic(symbol: string): AtomicUnitDef;
  listAtomics(): readonly AtomicUnitDef[];
}

export const defaultUnitRegistry: UnitRegistry;
import { defaultUnitRegistry } from "@vetwo/units";

defaultUnitRegistry.registerAtomic({
  symbol: "mol",
  dimension: { N: 1 },   // pick whichever base dimension fits your domain
  toBaseFactor: 1,
  label: "mole",
});

// "mol/L" now parses automatically — no other change required
// Isolated registry for tests:
const testRegistry = new UnitRegistry();
Quantity.of(1, "kg", testRegistry);

CalculationRuleRegistry

A generic, named registry for domain calculation rules layered on top of Quantity math. The core package doesn't know what "molarity" or "crude protein contribution" means — domain packages register named rules here instead of hard-coding formulas inline.

class CalculationRuleRegistry {
  register(key: string, rule: (...inputs: Quantity[]) => Quantity): void;
  has(key: string): boolean;
  resolve(key: string): (...inputs: Quantity[]) => Quantity;
  run(key: string, ...inputs: Quantity[]): Quantity;
  listKeys(): string[];
}

export const defaultCalculationRuleRegistry: CalculationRuleRegistry;
import { defaultCalculationRuleRegistry, Quantity } from "@vetwo/units";

defaultCalculationRuleRegistry.register("chemistry.molarity", (moles, volume) =>
  moles.divide(volume)
);

defaultCalculationRuleRegistry.run(
  "chemistry.molarity",
  Quantity.of(0.5, "mol"),
  Quantity.of(2, "L")
); // 0.25 mol/L

formatQuantity()

Renders a Quantity as a display string — intended for report/UI layers, never for calculation.

function formatQuantity(q: Quantity, opts?: {
  decimals?: number;   // default 2
  showBasis?: boolean; // default true
  locale?: string;     // uses Intl number formatting if provided
}): string;
formatQuantity(Quantity.of(12.345, "Mcal/day"), { decimals: 1 }); // "12.3 Mcal/day"
formatQuantity(Quantity.of(15, "% DM"));                          // "15.00 % DM"
formatQuantity(Quantity.of(1234.5, "cur/day"), { locale: "en-US" }); // "1,234.5 cur/day"

serializeQuantity() / deserializeQuantity()

Round-trip a Quantity through plain JSON — for database storage or API payloads.

function serializeQuantity(q: Quantity): { value: number; unit: string };
function deserializeQuantity(data: { value: number; unit: string }, registry?: UnitRegistry): Quantity;
const json = serializeQuantity(Quantity.of(12, "% DM"));
// { value: 12, unit: "% DM" }

const restored = deserializeQuantity(json);

Type guards

function isQuantity(value: unknown): value is Quantity;
function isUnit(value: unknown): value is Unit;
function isRatioUnit(unit: Unit): boolean;       // % / ppm / any dimensionless compound
function isSameDimension(a: DimensionVector, b: DimensionVector): boolean;
function isMoneyQuantity(q: Quantity): boolean;  // pure Currency dimension only
if (isQuantity(payload)) { /* safe to call .to(), .multiply(), etc. */ }

Testing utilities

function assertQuantityClose(actual: Quantity, expected: Quantity, epsilon?: number, message?: string): void;
function approxEqual(a: number, b: number, epsilon?: number): boolean;
import { assertQuantityClose, Quantity } from "@vetwo/units";

assertQuantityClose(result, Quantity.of(1200, "g/day"), 1e-6);

Errors

All errors extend UnitEngineError, so you can catch broadly or narrowly.

class UnitEngineError extends Error {}

class UnitMismatchError extends UnitEngineError {}        // add/subtract, different dimensions
class UnsupportedUnitError extends UnitEngineError {}     // unknown unit symbol
class DimensionError extends UnitEngineError {}            // wrong dimension for the operation
class ConversionError extends UnitEngineError {}           // conversion failed (e.g. basis clash)
class ImpossibleConversionError extends UnitEngineError {} // no shared dimension, no bridging rule
class RuleNotFoundError extends UnitEngineError {}          // unregistered CalculationRuleRegistry key
try {
  Quantity.of(1, "kg").add(Quantity.of(1, "Mcal"));
} catch (e) {
  if (e instanceof UnitMismatchError) {
    // handle: "Unit mismatch: cannot combine "kg" with "Mcal" (different dimensions)."
  }
}

Extending the engine for a new domain

@vetwo/units was designed so a brand-new scientific/financial domain can be added without ever touching this package's source:

  1. Register any new atomic units you need via defaultUnitRegistry.registerAtomic().
  2. Register your domain's formulas via defaultCalculationRuleRegistry.register().
  3. Wrap both in your own package (e.g. @vetwo/nutrition-units, or a @you/chemistry-units) that exposes a friendly facade — the way NutritionMath and CoefficientResolver do in @vetwo/nutrition-units.

This package should never contain nutrition, chemistry, or finance-specific code — if you find yourself wanting to add that here, it belongs in a domain package instead.


Design principles

  • No magic numbers outside one file. Every raw conversion constant lives in a single, documented internal file; nowhere else in this package (or any consuming application) should contain a bare conversion factor.
  • Single Responsibility per module — parsing, registry, math, formatting, and serialization are each isolated.
  • Open/Closed — new units and new domain rules are added by registering, never by editing internals.
  • Zero domain knowledge — this package is safe to reuse in any scientific or financial context.

Requirements

  • TypeScript ^5.x
  • No runtime dependencies

License

MIT