Karat-aware gold weight and valuation. Zero dependencies.
Gold in Egypt and the Gulf is bought by gram and karat — 21K is the default shop grade, not 24K. Spot is quoted in USD per troy ounce of pure gold. Every commodity library assumes ounces and ignores karat, so the conversion is left to the caller — and the caller gets it wrong the same way every time.
import { valueFromSpot, pricePerGram } from 'karat-gold';
// 100g of 21K, spot $2,400/oz, reported in EGP
valueFromSpot({
spotUsdPerOz: 2400,
weightGrams: 100,
karat: '21K',
fxUsdToBase: 48.5,
});
pricePerGram(2400, '21K'); // what a shop quotes, per gramvalue = (spot / 31.1035) × 24K-equivalent grams × FX
The 24K conversion lives inside valueFromSpot. Pass the weight as written
on the receipt, not a pre-converted one.
Applying purity twice is the bug this library exists to prevent, and it does not announce itself. 21K double-applied reads as 76.6% of the true value — small enough to look like a market move, large enough to matter. There is a test pinning exactly that ratio.
purity() returns karat / 24 as a division, not a rounded decimal. 21/24 is
0.875 exactly, but 22/24 is 0.91666…. A table of four-decimal constants
loses about 3 grams on a kilo of 22K.
KARATS |
['24K','22K','21K','18K','14K','10K'], highest purity first |
purity(karat) |
fraction of pure gold, karat / 24 |
to24kGrams(weightGrams, karat) |
physical grams → 24K-equivalent grams |
from24kGrams(pureGrams, karat) |
the inverse |
valueFromSpot({ spotUsdPerOz, weightGrams, karat, fxUsdToBase? }) |
value of a holding |
pricePerGram(spotUsdPerOz, karat, fxUsdToBase?) |
per-gram quote |
isKarat(value) |
type guard for untrusted input |
GRAMS_PER_TROY_OUNCE |
31.1035 |
Negative, NaN, and Infinity inputs throw RangeError. Zero weight is valid
and returns 0 — an empty holding is not an error.
The same rule as an IMMUTABLE function, for when valuation belongs in the
database: sql/gold_weight_24k_equiv.sql.
fxUsdToBase is a parameter rather than a lookup on purpose. Lock each holding
to the rate on the day it was recorded and last year's numbers stay put; resolve
FX at read time and they quietly rewrite themselves every morning.
Requires Node 22.6+ (native TypeScript, no build step, no dependencies).
npm testMIT