- ✅ Type-safe tracking of sap collections and boil sessions with branded identifiers
- ✅ Rule of 86 calculations for theoretical syrup yield prediction
- ✅ Efficiency metrics including boil ratios, fuel consumption, and season summaries
- ✅ Zero dependencies — runs on Bun and Node.js 20+ with only built-in modules
- ✅ Full TypeScript support with strict mode and comprehensive type definitions
npm install @adametherzlab/maple-tap
# or
bun add @adametherzlab/maple-tap// REMOVED external import: import { createStore, addSapCollection, ruleOf86YieldLiters } from "@adametherzlab/maple-tap";
// REMOVED external import: import type { TapId, TreeId, Volume, BrixReading } from "@adametherzlab/maple-tap";
const store = createStore();
const updatedStore = addSapCollection(store, {
tapId: "tap-001" as TapId,
treeId: "sugar-maple-01" as TreeId,
volume: 20 as Volume,
brix: 2.1 as BrixReading,
collectedAt: new Date()
});
const theoreticalYield = ruleOf86YieldLiters(20 as Volume, 2.1 as BrixReading);
console.log(`Expected syrup output: ${theoreticalYield.toFixed(2)} liters`);The Rule of 86 is the time-honored approximation used by sugar makers to estimate syrup yield from raw sap. The formula derives from the fact that finished maple syrup contains approximately 66% sugar (66°Bx).
Formula: (sapVolume × sapBrix) / 66 = syrupVolume
Alternative form: 86 ÷ sapBrix = gallons of sap needed per gallon of syrup
For example, with sap measuring 2°Bx (2% sugar), you need approximately 43 gallons of sap to produce 1 gallon of syrup (86 ÷ 2 = 43). This rule helps producers estimate fuel needs, collection schedules, and expected output before firing up the evaporator.
| Type | Description |
|---|---|
TapId |
Branded string identifier for a tap spout |
TreeId |
Branded string identifier for a maple tree |
SessionId |
Branded string identifier for a boil session |
SeasonId |
Branded string identifier for a production season (e.g., "2024") |
BrixReading |
Sugar content in degrees Brix (0-100), where 2.0 represents 2% sugar |
Volume |
Liquid volume as a branded number (unit determined by preferences) |
DurationMinutes |
Time duration in minutes |
VolumeUnit |
'liters' | 'gallons' |
TemperatureUnit |
'celsius' | 'fahrenheit' |
FuelType |
'wood' | 'propane' | 'oil' | 'electric' | 'other' |
UnitPreferences |
Configuration for measurement units and display formats |
SapCollection |
Record of sap volume, Brix reading, tap/tree IDs, and collection timestamp |
BoilSession |
Record of input/output volumes, fuel usage, duration, and date |
EfficiencyMetrics |
Calculated boil ratio, yield percentage, and fuel efficiency |
SeasonSummary |
Aggregated totals and averages for a complete production season |
MapleTapConfig |
Application configuration options |
MapleTapStore |
Immutable data store containing all collections and sessions |
const store = createStore();- Parameters:
store— current store;collection— SapCollection data (id auto-generated) - Throws:
RangeErrorif volume ≤ 0 or Brix outside 0-100;Errorif date invalid
const newStore = addSapCollection(store, {
tapId: "tap-1" as TapId,
treeId: "tree-1" as TreeId,
volume: 10 as Volume,
brix: 2.5 as BrixReading,
collectedAt: new Date()
});- Parameters:
store— current store;session— BoilSession data (id auto-generated) - Throws:
RangeErrorif volumes invalid, fuel negative, or duration ≤ 0
const newStore = addBoilSession(store, {
date: new Date(),
sapInputVolume: 40 as Volume,
syrupOutputVolume: 1 as Volume,
fuelType: "wood",
fuelUsed: 5,
durationMinutes: 180 as DurationMinutes
});const collections = getCollectionsByTap(store, "tap-1" as TapId);const collections = getCollectionsByTree(store, "tree-1" as TreeId);const sessions = getBoilSessions(store);const newStore = removeCollection(store, "uuid-string");const newStore = removeBoilSession(store, "uuid-string" as SessionId);- Parameters:
sapVolume— liters of raw sap;sapBrix— sugar content (0-100) - Returns: Theoretical syrup yield in liters
- Throws:
RangeErrorif volume negative or Brix outside 0-100
const yield = ruleOf86YieldLiters(100 as Volume, 2.0 as BrixReading); // ~3.03 liters- Throws:
RangeErrorif brix outside 0-100
const percent = brixToSugarPercent(2.5 as BrixReading); // 2.5- Throws:
RangeErrorif volume negative;Errorif invalid unit combination
const gallons = convertVolume(10 as Volume, "liters", "gallons"); // ~2.64- Parameters:
session— BoilSession data;averageBrix— average sugar content of input sap - Returns: Metrics including boil ratio, yield percentage, and fuel consumption rates
- Throws:
RangeErrorfor invalid inputs
const metrics = calculateEfficiencyMetrics(session, 2.2 as BrixReading);- Throws:
Errorif both arrays are empty
const summary = calculateSeasonSummary("2024" as SeasonId, collections, sessions);import {
createStore,
addSapCollection,
addBoilSession,
ruleOf86YieldLiters,
calculateEfficiencyMetrics,
calculateSeasonSummary,
convertVolume
} from "@adametherzlab/maple-tap";
// REMOVED external import: import type { TapId, TreeId, Volume, BrixReading, SeasonId } from "@adametherzlab/maple-tap";
// Initialize season
let store = createStore();
const seasonId = "2024" as SeasonId;
// Early season collections (lower sugar content)
store = addSapCollection(store, {
tapId: "tap-north-01" as TapId,
treeId: "tree-001" as TreeId,
volume: 15 as Volume,
brix: 1.8 as BrixReading,
collectedAt: new Date("2024-03-01")
});
store = addSapCollection(store, {
tapId: "tap-north-02" as TapId,
treeId: "tree-002" as TreeId,
volume: 22 as Volume,
brix: 2.2 as BrixReading,
collectedAt: new Date("2024-03-01")
});
// Peak season (higher sugar)
store = addSapCollection(store, {
tapId: "tap-north-01" as TapId,
treeId: "tree-001" as TreeId,
volume: 18 as Volume,
brix: 3.1 as BrixReading,
collectedAt: new Date("2024-03-15")
});
// First boil session: 55 liters total input
store = addBoilSession(store, {
date: new Date("2024-03-02"),
sapInputVolume: 37 as Volume, // 15 + 22
syrupOutputVolume: 1.25 as Volume,
fuelType: "wood",
fuelUsed: 8, // cords or arbitrary units
durationMinutes: 240 as DurationMinutes
});
// Calculate theoretical vs actual for the first boil
const theoreticalYield = ruleOf86YieldLiters(37 as Volume, 2.0 as BrixReading); // ~1.12 liters
console.log(`Theoretical: ${theoreticalYield.toFixed(2)}L, Actual: 1.25L`);
// Season summary
const collections = [...store.collections];
const sessions = [...store.boilSessions];
const summary = calculateSeasonSummary(seasonId, collections, sessions);
console.log(`Total sap collected: ${summary.totalSapVolume} liters`);
console.log(`Total syrup produced: ${summary.totalSyrupVolume} liters`);
console.log(`Average sugar content: ${summary.averageBrix.toFixed(1)}°Bx`);
console.log(`Overall efficiency: ${(summary.efficiency.yieldPercentage * 100).toFixed(1)}%`);
// Convert to gallons for the US market
const totalGallons = convertVolume(summary.totalSyrupVolume as Volume, "liters", "gallons");
console.log(`Season yield: ${totalGallons.toFixed(2)} gallons`);See CONTRIBUTING.md
MIT (c) AdametherzLab