Skip to content
heapbyte - A name of excellence

Engineering · 6 October 2026

Twenty-three and three-eighths is one size, not five

Three customers order the same walnut shelf. One types 23 3/8 into the width field. One types 23.375, because that is what the tape measure app said. One is on a phone, where the keyboard offers ⅜ as a single character. All three mean the same piece of wood. The cart holds three different strings, the price table is looked up at three different places, and the cut list on the workshop wall has a shelf at 23.375 next to a shelf at 23-3/8 that somebody has to stop and check.

7 min read
Written by the HeapByte engineering team

Fractional Inches in a Shopify Configurator

The measurement is the product

In a configurator that sells by dimension, the measurement is not a detail of the order. It is the product. There is no variant called 23-3/8 inches; there is a material, a thickness, and two numbers the customer typed, and everything downstream — the price, the cart line, the production ticket — is derived from those numbers.

Which means the numbers have to mean one thing everywhere. The Custom Floating Shelves configurator sells shelves from 3 by 9 inches up to 16 by 96, in thicknesses from 1-3/4 inches to 6, and the case study’s engineering notes put the requirement plainly: the configurator supports dimensional ranges and fractional measurements “while preserving consistent pricing lookup and cart representation”. That is the whole job in one clause, and it is harder than it reads.

The floats are fine. The strings are not

The first instinct is to blame floating point, and for ruler fractions it is the wrong instinct. Halves, quarters, eighths and sixteenths are all exact in binary: 23 plus three-eighths is 23.375 to the last bit, every time. A configurator storing 23.375 as a number has not lost anything.

What goes wrong is the text. A customer can write one size several ways, and Shopify’s cart treats line items with the same variant but different properties as separate lines. Two shelves that differ only in how the width was spelled sit in the cart as two lines rather than one with a quantity of two. Properties are also replaced wholesale whenever a cart request includes them, so a later update that writes the width back in a different format quietly changes what the order says.

And the decimal that is not a fraction is a real hazard. Somebody types 23.3. That is not a mark on any tape measure in the United States; it is either a typo for 23.375, a metric habit, or a guess. A configurator that rounds it to the nearest sixteenth has made a decision about a piece of furniture on the customer’s behalf.

Parse once, at the edge

So the measurement is parsed once, at the point the customer enters it, into an integer count of sixteenths of an inch, and nothing after that point ever sees the customer’s spelling again.

The parser accepts the honest forms — 23 3/8, 23-3/8, 23.375, the ⅜ glyph, a trailing inch mark — and refuses the rest. A decimal is accepted only if it lands exactly on a sixteenth. A fraction is accepted only if its denominator is a ruler’s: two, four, eight or sixteen. Three-sevenths is refused, not approximated, because nobody’s tape is marked in sevenths and a value nobody could have measured is a value somebody mistyped.

The same function has to run everywhere the measurement is read, not only where it is typed. The browser runs it to give the customer immediate feedback; whatever processes the order runs it again, because a cart’s copy of a property is just text that can be rewritten after the fact. Two parsers that disagree about one edge case — a trailing space, the ⅜ glyph — produce exactly the inconsistency the parsing was meant to remove.

Add-ons make this sharper. On the CFS build, base products and paid add-ons can appear as separate priced lines in the cart, each preserving the configuration context. An add-on that belongs to a particular shelf has to carry that shelf’s size in the same canonical form as the shelf’s own line, or nothing downstream can tell which add-on goes with which board. Matching by spelling is matching by luck.

ts
/** Every length is held as an integer count of sixteenths of an inch. */
const PER_INCH = 16;

const GLYPH: Record<string, string> = {
  "¼": "1/4", "½": "1/2", "¾": "3/4", "⅛": "1/8", "⅜": "3/8", "⅝": "5/8", "⅞": "7/8",
};

/** Parse what a customer typed into sixteenths, or refuse. Never round a guess. */
export function parseInches(raw: string): number {
  let s = raw.trim().replace(/(["”]|in(ches)?)$/i, "").trim();
  s = s.replace(/[¼½¾⅛⅜⅝⅞]/g, (g) => " " + GLYPH[g]).trim();

  // 23.375 — decimals are accepted only if they land exactly on a sixteenth.
  let m = /^(\d+)(?:\.(\d+))?$/.exec(s);
  if (m) {
    const sixteenths = Number(s) * PER_INCH;
    if (!Number.isInteger(sixteenths)) throw new Error(`"${raw}" is not a whole number of sixteenths`);
    return sixteenths;
  }

  // 23 3/8, 23-3/8, or 3/8 on its own.
  m = /^(?:(\d+)[\s-]+)?(\d+)\/(\d+)$/.exec(s);
  if (!m) throw new Error(`"${raw}" is not a measurement`);
  const [whole, num, den] = [Number(m[1] ?? 0), Number(m[2]), Number(m[3])];
  if (![2, 4, 8, 16].includes(den)) throw new Error(`"${raw}" is not a ruler fraction`);
  if (num >= den) throw new Error(`"${raw}" has an improper fraction`);
  return whole * PER_INCH + num * (PER_INCH / den);
}

/** The one spelling used everywhere after parsing: cart, order, cut list. */
export function canonical(sixteenths: number): string {
  const whole = Math.floor(sixteenths / PER_INCH);
  let num = sixteenths % PER_INCH;
  let den = PER_INCH;
  if (num === 0) return `${whole}"`;
  while (num % 2 === 0) { num /= 2; den /= 2; }
  return whole === 0 ? `${num}/${den}"` : `${whole}-${num}/${den}"`;
}

/** The size the price table is looked up at: rounded up to its step, never down. */
export function priceKey(sixteenths: number, stepSixteenths: number): number {
  if (!Number.isInteger(stepSixteenths) || stepSixteenths < 1) throw new Error("step must be a positive integer");
  return Math.ceil(sixteenths / stepSixteenths) * stepSixteenths;
}
The surprise when writing this was that the obvious villain is innocent. Every ruler fraction — halves through sixteenths — is exact in binary floating point, so 23 plus three-eighths really is 23.375 to the last bit. What is not exact is everything around it: a customer typing 23.3, which is no mark on any tape measure, and three honest spellings of one size that a cart treats as three different things. So the unit is an integer count of sixteenths, the decimal branch refuses anything that does not land on one, and nothing after this function ever sees the customer’s spelling again.

Round up, and say so

The price table is the other place a measurement gets bent. Few businesses price every sixteenth separately; the table steps by the inch, or the half inch, and a width between steps has to be looked up somewhere.

The rule in the code is to round up to the next step, never down, and to treat the step as a business decision rather than a constant buried in a function. Rounding down means a 23-1/16 inch shelf is priced as a 23 inch one, and across a catalogue of long shelves that is a slow, permanent discount nobody chose. Rounding up means the customer pays for the next increment of timber, which is usually what the workshop actually cuts from. Either way, the size the customer typed is what goes on the cut list. Only the price lookup is stepped.

One spelling downstream

After parsing, there is exactly one spelling: 23-3/8", reduced, generated by the same function every time. That string is what goes into the line item properties, what appears on the order, and what the workshop reads. Six sixteenths becomes three-eighths before anybody sees it.

It is also what any protection on the configured price has to cover. If the price is signed or verified on its way to the cart — the problem of where a computed price is allowed to live — the canonical string is the thing being signed, so the same shelf always produces the same signature rather than one per spelling.

What this does not handle

It is inches only. A store selling in millimetres has an easier parser and the same canonical-form problem, and a store selling in both has a conversion step that is genuinely lossy — 23-3/8 inches is 593.725 millimetres, which nobody can cut — and needs its own rounding rule.

It does not check ranges. Whether 3 inches is too shallow for a given material, or 96 too long for a given thickness, is the material’s rule rather than the measurement’s, and on the CFS build material-specific restrictions sit in their own layer for exactly that reason.

And it does not decide the step. Whole inches, half inches or sixteenths is a pricing decision, and the code takes it as an argument because it will change.

When this needs an engineer

It does not, if every dimension is a fixed choice from a list. A drop-down of 24, 30 and 36 inches is three variants or three property values, and the spelling problem cannot arise because the customer never types anything.

It does when customers type the measurement, when the same size can reach the cart by more than one route, and when the order becomes a production instruction that somebody acts on with a saw. That is the configurator work in our configurator case studies, and it is what product configurator development has to get right before it gets anything else right.

Send us the store and the symptom.

---

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.