ng-nutrition-score
v1.0.1
Published
Nigeria nutrition scoring engine. Converts raw product nutrition facts into a letter grade (A–E) using WHO AFRO nutrient profile thresholds calibrated to the Nigerian dietary context.
Maintainers
Readme
Nigeria-nutrition-score
Nigeria nutrition scoring engine. Converts food product nutrition facts into a letter grade A–E using WHO AFRO nutrient profile thresholds calibrated to the Nigerian dietary context.
Features
- Letter grades A–E — based on WHO nutrient profile model for the African region and NAFDAC Food Labelling Regulations 2005
- Category-aware thresholds — beverages, cereals, dairy, meat, oils, sauces, and more each have their own grade boundaries
- Full scoring breakdown — per-nutrient points for transparency
- Dual ESM + CJS — works in modern Node (ESM), legacy Node (CommonJS), and bundlers
- Optional MongoDB pipeline — a
/dbsubpath maps rawDbProductdocuments to scored results without any MongoDB runtime dependency - Zero dependencies — pure TypeScript, no external runtime packages
Installation
npm install ng-nutrition-scoreQuick Start
Scoring from NutritionFacts directly
Use this if you already have nutrition data in the right shape (e.g. from a REST API, form input, or CSV).
import { calculateNutritionScore } from "ng-nutrition-score";
const result = calculateNutritionScore({
energyKcal: 364,
totalFatG: 1.2,
saturatedFatG: 0.3,
transFatG: 0,
totalCarbohydratesG: 80,
totalSugarsG: 0.5,
dietaryFibreG: 2.0,
proteinG: 7.0,
sodiumMg: 2,
category: "cereal",
});
console.log(result.grade); // "B"
console.log(result.label); // "Good"
console.log(result.recommendation); // "Good nutritional profile..."
console.log(result.rawScore); // e.g. 5
console.log(result.breakdown); // per-nutrient point arrayMongoDB pipeline (server-side and client-side)
Use this if your product data comes from MongoDB in the DbProduct shape. The /db subpath handles all label normalisation: per-serving → per-100g, unit conversions (kJ→kcal, salt→sodium, mg→g), 30+ nutrient name aliases, and sanity clamps.
import { runScoringPipeline } from "ng-nutrition-score/db";
// products: DbProduct[] fetched from MongoDB
const scored = runScoringPipeline(products);
for (const p of scored) {
console.log(`${p.productName}: ${p.score.grade} (${p.score.label})`);
if (p.warnings.length) {
console.log(" Conversion notes:", p.warnings);
}
}You can also use the mapper independently:
import {
mapDbProductToNutritionFacts,
hasNoNutritionData,
} from "ng-nutrition-score/db";
if (!hasNoNutritionData(product)) {
const { facts, warnings } = mapDbProductToNutritionFacts(product);
// facts is a NutritionFacts object ready for calculateNutritionScore()
}API Reference
Main entry point (ng-nutrition-score)
calculateNutritionScore(facts: NutritionFacts): NutritionScoreResult
Validates and scores a NutritionFacts object. Throws if required fields are missing or nutritionally impossible (e.g. saturated fat > total fat).
Algorithm:
rawScore = negativePoints − positivePoints| Negative nutrients (penalised) | Positive nutrients (rewarded) | |-------------------------------|-------------------------------| | Energy (kcal) | Dietary fibre (g) | | Saturated fat (g) | Protein (g) | | Trans fat (g) | | | Total/added sugars (g) | | | Sodium (mg) | |
UNSCORED_RESULT: NutritionScoreResult
A sentinel result with grade: "N/A" and scorable: false. Returned by the pipeline when a product has no nutrition data on its label.
saltToSodium(saltG: number): number
Converts salt in grams to sodium in mg (saltG × 400).
resolveGrade(rawScore, category): { grade, label, recommendation }
Maps a raw score to a letter grade for a given food category.
DB subpath (ng-nutrition-score/db)
When to use: Import from
/dbonly if your data originates from a MongoDB collection in theDbProductshape described below. If you are constructingNutritionFactsyourself, use the main entry point.
runScoringPipeline(products: DbProduct[]): ScoredProduct[]
Full pipeline from MongoDB documents to scored results. Products with empty nutrition arrays receive UNSCORED_RESULT.
mapDbProductToNutritionFacts(product: DbProduct): MappedNutritionResult
Maps a single DbProduct to NutritionFacts. Returns { facts, warnings } where warnings lists any unit conversions applied.
Transformations applied:
- Per-serving → per-100g normalisation (
Per 50g,Per 25cl,Per 1 tsp, etc.) - Energy: kJ → kcal (
÷ 4.184) - Sodium: g → mg (
× 1000), or derived from salt (salt g × 400) - Macronutrients declared as
%: treated asg/100g - Protein/fibre/fat declared in
mg: converted tog - 30+ nutrient name variants aliased to 10 canonical keys
- Sanity clamps: saturated fat ≤ total fat; sugars ≤ total carbohydrates
hasNoNutritionData(product: DbProduct): boolean
Returns true if the product's nutrition array is empty (e.g. table salt, pure spices).
Food Categories
| Category value | Description |
|-------------------|--------------------------------------------------|
| general | Default; snacks, confectionery, cooking ingredients |
| beverage | Drinks — stricter sugar thresholds (WHO AFRO) |
| dairy | Dairy products, dairy alternatives, medical nutrition drinks |
| cereal | Cereals, grains, staples, ready meals, baby food |
| meat | Meat, poultry, seafood |
| fruit_vegetable | Fruits and vegetables — more lenient boundaries |
| fat_oil | Oils, fats, spreads — higher fat thresholds |
| sauce_condiment | Sauces, condiments, seasonings |
Grade Scale
| Grade | Label | Meaning | |-------|-----------|-------------------------------------------------------| | A | Excellent | Strong nutritional quality; suitable for regular consumption | | B | Good | Good profile; suitable as part of a balanced diet | | C | Fair | Moderate quality; consume in reasonable portions | | D | Poor | Low quality; limit intake | | E | Avoid | Poor profile; avoid regular consumption |
Grade boundaries are category-specific. For example, beverages are judged more strictly than oils.
NutritionFacts Shape
interface NutritionFacts {
energyKcal: number; // kcal per 100g/100ml
totalFatG: number; // g per 100g
saturatedFatG: number; // g per 100g
transFatG: number; // g per 100g
totalCarbohydratesG: number; // g per 100g
totalSugarsG: number; // g per 100g
addedSugarsG?: number; // g per 100g (optional; falls back to totalSugars)
dietaryFibreG: number; // g per 100g
proteinG: number; // g per 100g
sodiumMg: number; // mg per 100g
category?: FoodCategory; // defaults to "general"
}Regulatory Basis
Thresholds are based on:
- WHO Nutrient Profile Model for the African Region (2022)
- NAFDAC Food Labelling Regulations 2005 (as amended)
- Calibrated to the Nigerian dietary context where staple foods (yam, cassava, plantain, rice) sit naturally in the B–C range
License
MIT
