🥤BevFacts

Technical / Part 3

Part 3 — Rounding & Daily Values

How computed values become declared values: the 21 CFR 101.9(c) rounding rules, the DV tables, and the exact strings that go on the label.

DBNF-1 · Part 3 Normative Live — exported as BevFacts.round.*

3.1Order of operations

The single most common labeling bug is rounding too early. The normative order is:

compute (Part 2, full precision)
   ├──▶ %DV      = round( unrounded_value / DV × 100 )      // from UNROUNDED values
   └──▶ declared = declaration_round( unrounded_value )      // §3.2, once

%DV is computed from unrounded values, then rounded to a whole percent. Computing %DV from the already-declared mass is nonconforming — it compounds two roundings and can shift the percentage by several points at beverage-typical magnitudes.

3.2Declaration rounding rules

Ties round half-up (2.5 g → 3 g at 1 g increments), matching FDA practice. The functions below are exported verbatim by the reference implementation as BevFacts.round.*.

QuantityRuleFunction
Calories< 5 → 0 · ≤ 50 → nearest 5 · > 50 → nearest 10round.calories
Total / sat / trans fat (g)< 0.5 → 0 · < 5 → nearest 0.5 · ≥ 5 → nearest 1round.fat
Cholesterol (mg)< 2 → 0 · 2–5 → "less than 5" · > 5 → nearest 5round.cholesterol
Sodium (mg)< 5 → 0 · 5–140 → nearest 5 · > 140 → nearest 10round.sodium
Carb / fiber / sugars / added / protein (g)< 0.5 → 0 · < 1 → "less than 1" · ≥ 1 → nearest 1round.gram
% Daily Valuenearest whole percent (from unrounded mass, §3.1)
Caffeine (mg)< 1 → 0 · 1–5 → "less than 5" · > 5 → nearest 5DBNF rule; no FDA equivalent exists
Note The caffeine rule is DBNF's own: decaf drinks legitimately carry 2–7 mg per shot, and "3 mg" false-precision on a label invites challenges the data can't support. less than 5 communicates "effectively decaf" honestly.

3.3Daily values (2,000 kcal reference diet)

NutrientDVShown on label?
Total fat78 g%DV shown
Saturated fat20 g%DV shown
Trans fatmass only, never %DV
Cholesterol300 mg%DV shown
Sodium2,300 mg%DV shown
Total carbohydrate275 g%DV shown
Dietary fiber28 g%DV shown
Total sugarsmass only, never %DV
Added sugars50 g%DV shown — the number DBNF exists for
Protein50 gmass only on beverage labels
Caffeineno DV; 400 mg/day FDA guidancemass only; guidance %MAY appear in companion UI, never on-label

3.4%DV computation

pctDV(nutrient) = round_half_up( unrounded(nutrient) / DV(nutrient) × 100 )   // whole %

Zero declarations still show 0% where the row carries %DV. Values over 100% print as-is (128%) — capping at 100% is nonconforming; the whole point is that one drink can exceed a day.

3.5Declared-string grammar

The exact strings, in ABNF. No locale-dependent formatting inside the value strings; unit is attached without a space, matching FDA convention.

declared-mass  = ( "less than " unit-num / unit-num ) unit
unit-num       = 1*4DIGIT [ "." DIGIT ]        ; "4.5"
unit           = "g" / "mg"
declared-cal   = 1*4DIGIT                       ; no unit on the label
declared-pct   = 1*3DIGIT "%"
added-row      = "Includes " declared-mass " Added Sugars"

Examples: 4.5g · less than 1g · 170mg · Includes 43g Added Sugars · 86%.

3.6Warning thresholds

Warnings are machine-readable flags in the DBNF document (Part 4); companion UIs decide presentation. Thresholds evaluate against unrounded values.

FlagConditionUI requirement
added_sugars_over_50pct_dvaddedSugars ≥ 25 gSHOULD surface a caution
added_sugars_over_100pct_dvaddedSugars ≥ 50 gMUST surface a caution
caffeine_over_half_dailycaffeine ≥ 200 mgSHOULD surface
caffeine_over_dailycaffeine ≥ 400 mgMUST surface; SHOULD require confirmation on kiosks
clamped_negative, energy_reconciliation_failedPart 2 §2.7engine diagnostics; MUST NOT be shown to consumers

3.7Test vectors

Verifiable in the console via BevFacts.round.*:

CallExpectedWhy
round.calories(4.9)0< 5 → 0
round.calories(47.5)50≤ 50, nearest 5, half-up
round.calories(324)320> 50, nearest 10
round.fat(4.74)"4.5"< 5, nearest 0.5
round.fat(5.4)"5"≥ 5, nearest 1
round.cholesterol(3)"less than 5"2–5 band
round.sodium(138)"140"5–140, nearest 5
round.sodium(146)"150"> 140, nearest 10
round.gram(0.7)"less than 1"0.5–1 band
round.gram(0.4)"0"< 0.5 → 0