"use strict"; function round2(x) { return Math.round(x * 100) / 100; } const CUM_TWELFTHS = [0, 1, 3, 6, 9, 11, 12]; // hour marks 0..6, cumulative twelfths of the range function estimateHeight(startTimeHours, startHeight, endTimeHours, endHeight, queryTimeHours) { const duration = endTimeHours - startTimeHours; const elapsed = queryTimeHours - startTimeHours; const clampedElapsed = Math.max(0, Math.min(duration, elapsed)); const scaledElapsed = (clampedElapsed / duration) * 6; // map to the canonical 6-hour rule-of-twelfths timeline const h = Math.min(5, Math.floor(scaledElapsed)); const frac = scaledElapsed - h; const twelfthsAtH = CUM_TWELFTHS[h]; const twelfthsAtH1 = CUM_TWELFTHS[h + 1]; const interpolatedTwelfths = twelfthsAtH + frac * (twelfthsAtH1 - twelfthsAtH); const totalRange = endHeight - startHeight; return startHeight + totalRange * (interpolatedTwelfths / 12); } module.exports = { slug: "tide-height-rule-of-twelfths", title: "Tide height by the rule of twelfths", deck_category: "astronomy", description: "Estimates the tide height at a query time between a known high and low tide using the traditional rule-of-twelfths approximation.", inputs: [ { name: "start_time_hours", type: "number", unit: "hours", required: true, description: "Time of the first known tide (high or low), in decimal hours on any consistent timeline." }, { name: "start_height", type: "number", unit: "same unit as end_height", required: true, description: "Tide height at start_time_hours." }, { name: "end_time_hours", type: "number", unit: "hours", required: true, description: "Time of the next tide (the opposite of start), in decimal hours. Should be about 6 hours after start_time_hours." }, { name: "end_height", type: "number", unit: "same unit as start_height", required: true, description: "Tide height at end_time_hours." }, { name: "query_time_hours", type: "number", unit: "hours", required: true, description: "Time at which to estimate the height, between start_time_hours and end_time_hours." } ], outputs: [ { name: "estimated_height", unit: "same unit as inputs", description: "Estimated tide height at the query time." } ], steps: [ "The rule of twelfths splits the roughly 6-hour rise or fall between a high and a low tide into 6 hourly amounts of 1, 2, 3, 3, 2, and 1 twelfths of the total range, in order.", "Cumulative twelfths at hour marks 0 through 6 are therefore 0, 1, 3, 6, 9, 11, and 12.", "Scale the elapsed time between start_time_hours and query_time_hours onto a canonical 0-to-6-hour timeline (elapsed / actual duration * 6), since the real interval may not be exactly 6 hours.", "Within the hour that contains the scaled elapsed time, linearly interpolate between that hour's cumulative twelfths and the next hour's cumulative twelfths.", "estimated_height = start_height + (end_height - start_height) * (interpolated cumulative twelfths / 12).", "Round the result to 2 decimals.", "This is a classroom approximation only. Real tide predictions come from NOAA CO-OPS (https://tidesandcurrents.noaa.gov/) and account for the actual, non-uniform tidal curve and the true semidiurnal period (about 6 hours 12.5 minutes, not exactly 6 hours)." ], constants: [ { name: "cumulative_twelfths_by_hour", value: "[0,1,3,6,9,11,12]", unit: "twelfths of range", source: 0 } ], sources: [ { kind: "url", value: "https://tidesandcurrents.noaa.gov/", title: "Tides & Currents", publisher: "NOAA Center for Operational Oceanographic Products and Services (CO-OPS)", retrieved_utc: "2026-09-30T22:29:13Z", note: "Confirmed live: NOAA's authoritative tide prediction portal. The rule of twelfths itself is a longstanding navigational approximation (not a NOAA method); this card cites NOAA CO-OPS as the source of real, non-approximated tide predictions referenced in the steps above." } ], disclaimer: null, questionText(inputs) { return `Using the rule of twelfths, if the tide is ${inputs.start_height} at hour ${inputs.start_time_hours} and ${inputs.end_height} at hour ${inputs.end_time_hours}, what is the estimated height at hour ${inputs.query_time_hours}?`; }, run(inputs) { const { start_time_hours, start_height, end_time_hours, end_height, query_time_hours } = inputs || {}; if ([start_time_hours, start_height, end_time_hours, end_height, query_time_hours].some((v) => typeof v !== "number" || Number.isNaN(v))) { throw new Error("all of start_time_hours, start_height, end_time_hours, end_height, query_time_hours must be numbers"); } if (end_time_hours <= start_time_hours) throw new Error("end_time_hours must be after start_time_hours"); if (query_time_hours < start_time_hours || query_time_hours > end_time_hours) throw new Error("query_time_hours must be between start_time_hours and end_time_hours"); const height = round2(estimateHeight(start_time_hours, start_height, end_time_hours, end_height, query_time_hours)); return { answer: `${height.toFixed(2)}`, values: { estimated_height: height } }; }, examples: [ { name: "worked example", inputs: { start_time_hours: 0, start_height: 4.0, end_time_hours: 6, end_height: 1.0, query_time_hours: 2 }, expected: { answer: "3.25" }, check: "By hand: hour 2 falls exactly at the hour-2 mark, cumulative twelfths = 1+2 = 3 of 12, so 3/12 of the 3.0 m range (0.75 m) has fallen from the 4.0 m high: 4.0 - 0.75 = 3.25 m." }, { name: "test vector: rising tide, midpoint of hour 5", inputs: { start_time_hours: 0, start_height: 0.5, end_time_hours: 6, end_height: 5.0, query_time_hours: 4.5 }, expected: { answer: "4.25" }, check: "By hand: hour 4 mark is 9/12 and hour 5 mark is 11/12; the midpoint of hour 5 (query at 4.5h) interpolates to 10/12 of the 4.5 m range (3.75 m) above the 0.5 m low: 0.5 + 3.75 = 4.25 m." } ] };