Watt-hour segment ledger
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html><html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Watt-hour segment ledger</title><style>*{box-sizing:border-box}:root{--ink:#24333d;--muted:#586872;--line:#cfd9de;--accent:#27627b;--soft:#edf3f5;--paper:white;color-scheme:light}body{margin:0;background:#f2f4f4;color:var(--ink);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1100px;padding:28px 32px 64px;margin:auto}header{margin-bottom:20px}h1{font-size:clamp(1.5rem,3vw,2.1rem);letter-spacing:-.035em;line-height:1.18;margin:0 0 9px}h2{font-size:1.2rem;line-height:1.3;margin:0 0 15px}h3{font-size:1rem;margin:0 0 8px}p{margin:9px 0 16px}header p{margin-bottom:0}.eyebrow{font-size:.75rem;font-weight:750;text-transform:uppercase;letter-spacing:.12em;color:var(--accent);margin-bottom:8px}.muted,small{color:var(--muted)}.panel{background:var(--paper);border:1px solid var(--line);padding:24px;border-radius:12px}.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.triple{display:grid;grid-template-columns:2fr 1fr 1fr;gap:16px}.below{margin-top:24px}label{display:block;font-size:.9rem;font-weight:650}input,select,textarea,button{font:inherit}input,select,textarea{display:block;width:100%;min-width:0;margin-top:6px;padding:10px 12px;border:1px solid #879da8;border-radius:6px;background:white;color:var(--ink)}input,select{min-height:46px}textarea{resize:vertical;line-height:1.6}button{min-height:44px;border:1px solid var(--accent);border-radius:6px;padding:9px 15px;background:var(--accent);color:white;font-weight:650;cursor:pointer}button.secondary{background:white;color:var(--accent)}button:disabled{opacity:.5;cursor:not-allowed}button:hover:enabled{filter:brightness(.95)}:focus-visible{outline:3px solid #bb6b22;outline-offset:3px}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.result{padding:26px;background:var(--ink);color:white;border-radius:12px}.result .muted{color:#d1e0e7}.big{font-size:clamp(2rem,5vw,3.5rem);line-height:1.15;letter-spacing:-.04em;font-weight:750;font-variant-numeric:tabular-nums;margin:10px 0;overflow-wrap:anywhere}.formula{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}.error{min-height:24px;color:#a33028;font-weight:650;margin:9px 0 0}.status{font-size:.9rem;color:var(--muted)}.note{padding:12px 16px;background:#fff3de;border-left:3px solid #b67e38;color:#65481d}table{width:100%;border-collapse:collapse;font-size:.92rem}th{text-align:left;color:var(--muted);font-size:.83rem}th,td{padding:10px 8px;border-bottom:1px solid var(--line);vertical-align:top}th:first-child,td:first-child{padding-left:0}details{padding:16px 0;border-top:1px solid var(--line)}summary{font-weight:650;cursor:pointer}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:.87rem/1.6 ui-monospace,Consolas,monospace;background:#edf1f3;border:1px solid #d7e0e4;padding:18px;border-radius:7px}svg{display:block;width:100%;height:auto}.badge{display:inline-block;padding:3px 9px;background:var(--soft);color:var(--accent);font-size:.78rem;font-weight:700;border-radius:25px}.rule{height:1px;background:var(--line);margin:18px 0}[hidden]{display:none!important}a{color:var(--accent)}@media(max-width:720px){main{padding:22px 20px 45px}.grid{grid-template-columns:1fr;gap:18px}.panel{padding:20px}.triple{grid-template-columns:1fr 1fr}.triple>label:first-child{grid-column:1/-1}}@media(max-width:400px){main{padding:18px 16px 36px}.panel{padding:16px}.fields{gap:12px}.result{padding:20px}.big{font-size:2.2rem}th,td{padding:9px 5px}}.energy-total{background:#213f4b;color:white;display:flex;justify-content:space-between;align-items:center;gap:20px;padding:25px 28px;border-radius:10px}.energy-other{display:grid;gap:12px;font-size:1.3rem}.energy-row{display:grid;grid-template-columns:45px 1fr 1fr 1fr auto;gap:16px;align-items:end;border-bottom:1px solid var(--line);padding:16px 0}.energy-row>b{align-self:center}.energy-row button{margin-bottom:1px}.energy-row:first-child{padding-top:0}@media(max-width:720px){.energy-row{grid-template-columns:1fr 1fr}.energy-row>b{grid-column:1/-1}.energy-row>label:nth-of-type(3){grid-column:1}.energy-row>button{align-self:end}}@media(max-width:400px){.energy-total{padding:20px;align-items:flex-start;flex-direction:column;gap:4px}.energy-other{display:flex;gap:16px;flex-wrap:wrap;font-size:1rem}}</style></head><body><main><header><h1>Watt-hour segment ledger</h1><p>Each row assumes constant supplied power for its full stated interval.</p></header>
<section class="energy-total" aria-live="polite"><div><span>Energy in the declared rows</span><div class="big" id="total">45 Wh</div></div><div class="energy-other"><b id="kwh">0.045 kWh</b><b id="joules">162000 J</b></div></section>
<section class="panel below"><div id="rows"></div><div class="actions"><button type="button" id="add" class="secondary">Add row</button><button type="button" id="calculate">Calculate total</button><button type="button" id="reset" class="secondary">Reset to blank</button></div><p id="status" role="status" class="status"></p><p id="error" role="status" class="error"></p></section>
<section class="panel below"><h2>Duration and energy record</h2><div id="record"></div><p id="sum" class="formula"></p></section>
<section class="below"><h2>Separate fictional worked fixtures</h2><div class="actions"><button type="button" id="minutes" class="secondary">Load 30 W × 90 min</button><button type="button" id="seconds" class="secondary">Load 30 W × 5400 s</button><button type="button" id="segments" class="secondary">Load the two-segment 50 Wh example</button></div><p class="muted">The minute and second fixtures describe equivalent independent examples. Loading replaces the current ledger; it never adds both as separate intervals.</p></section>
<details class="below"><summary>Assumptions and input limits</summary><p>Wh = W × h; min ÷ 60 gives hours; s ÷ 3600 gives hours. kWh = Wh ÷ 1000; J = Wh × 3600. Exact fractions feed the total before any display rounding. Visible values use at most six places, marking approximations and tiny positives.</p><p>Keep 1–10 rows. Power and duration are nonnegative decimal values ≤1,000,000, up to six fractional places. Blank and unknown units are invalid even when power is zero. Every edit, add, or removal clears the total until Calculate. Reset leaves one blank row and no time unit. The sum does not validate interval overlap or establish measured consumption, device ratings, batteries, cost, emissions, or runtime. Entries are not saved.</p></details></main><script>const $=id=>document.getElementById(id),put=(id,s)=>{$(id).textContent=s};
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
function gcd(a,b){a=a<0n?-a:a;b=b<0n?-b:b;while(b){[a,b]=[b,a%b]}return a||1n;}
function R(n,d=1n){n=BigInt(n);d=BigInt(d);if(d===0n)throw Error('Undefined denominator.');if(d<0n){n=-n;d=-d}const g=gcd(n,d);return {n:n/g,d:d/g};}
function mul(a,b){return R(a.n*b.n,a.d*b.d)}function div(a,b){return R(a.n*b.d,a.d*b.n)}function add(a,b){return R(a.n*b.d+b.n*a.d,a.d*b.d)}function sub(a,b){return R(a.n*b.d-b.n*a.d,a.d*b.d)}function cmp(a,b){const x=a.n*b.d-b.n*a.d;return x<0n?-1:x>0n?1:0}
function parse(s,{max=1000000,positive=false,places=6}={}){const t=String(s).trim();if(!t)throw Error('Enter a value; blank is not zero.');const re=new RegExp('^[0-9]+(?:\.[0-9]{1,'+places+'})?$');if(!re.test(t))throw Error('Use digits and a US decimal point, up to '+places+' places; no signs, commas, or exponents.');const v=Number(t);if(!Number.isFinite(v)||v>max||(positive&&v===0))throw Error('Value must be '+(positive?'greater than zero':'nonnegative')+' and at most '+max+'.');const [a,b='']=t.split('.');return R(BigInt(a+b),10n**BigInt(b.length));}
function format(a,places=6){const neg=a.n<0n,n=neg?-a.n:a.n,scale=10n**BigInt(places),v=n*scale,q=v/a.d,rem=v%a.d,k=q+(rem*2n>=a.d?1n:0n);const f=(k%scale).toString().padStart(places,'0').replace(/0+$/,'');return {text:(neg&&k!==0n?'-':'')+(k/scale)+(f?'.'+f:''),approx:rem!==0n};}
function exact(a){let d=a.d;while(d%2n===0n)d/=2n;while(d%5n===0n)d/=5n;if(d!==1n)return a.n+'/'+a.d;let places=0,p=1n;while(p%a.d!==0n&&places<40){p*=10n;places++}return format(a,places).text;}
function display(a,places=6){if(a.n>0n&&cmp(a,R(1n,10n**BigInt(places)))<0)return '< '+format(R(1n,10n**BigInt(places)),places).text;const f=format(a,places);return (f.approx?'≈ ':'')+f.text;}
function listen(ids,fn){ids.forEach(id=>{ $(id).addEventListener('input',fn);$(id).addEventListener('change',fn); });}
let rows=[{id:'E01',power:'30',duration:'90',unit:'min'}],nextId=2;
function calculateRows(data){if(data.length<1||data.length>10)throw Error('Use 1 through 10 rows.');const seen=new Set(),result=data.map(row=>{if(!/^E\d{2,}$/.test(row.id)||seen.has(row.id))throw Error('Invalid or duplicate row ID.');seen.add(row.id);try{if(!['h','min','s'].includes(row.unit))throw Error('Choose h, min, or s.');const power=parse(row.power),duration=parse(row.duration),hours=div(duration,R(row.unit==='h'?1n:row.unit==='min'?60n:3600n));return {...row,hours,energy:mul(power,hours)};}catch(e){throw Error(row.id+': '+e.message)}});const total=result.reduce((s,r)=>add(s,r.energy),R(0n));return {rows:result,total,kwh:div(total,R(1000n)),joules:mul(total,R(3600n))};}
function renderRows(){ $('rows').innerHTML=rows.map(row=>'<section class="energy-row"><b>'+row.id+'</b><label for="power-'+row.id+'">Power (W)<input id="power-'+row.id+'" data-row="'+row.id+'" data-field="power" inputmode="decimal" value="'+esc(row.power)+'"></label><label for="duration-'+row.id+'">Elapsed time<input id="duration-'+row.id+'" data-row="'+row.id+'" data-field="duration" inputmode="decimal" value="'+esc(row.duration)+'"></label><label for="unit-'+row.id+'">Time unit<select id="unit-'+row.id+'" data-row="'+row.id+'" data-field="unit">'+[['','Choose unit'],['h','h'],['min','min'],['s','s']].map(([v,label])=>'<option value="'+v+'"'+(row.unit===v?' selected':'')+'>'+label+'</option>').join('')+'</select></label><button type="button" class="secondary" data-remove="'+row.id+'"'+(rows.length===1?' disabled':'')+' aria-label="Remove row '+row.id+'">Remove</button></section>').join('');$('add').disabled=rows.length>=10;}
function invalidate(){['total','kwh','joules'].forEach(id=>put(id,'—'));$('record').innerHTML='';put('sum','');put('error','');put('status','Ledger changed. Calculate to update all row energies and the total.');}
function calculate(){try{const r=calculateRows(rows);put('total',display(r.total)+' Wh');put('kwh',display(r.kwh)+' kWh');put('joules',display(r.joules)+' J');$('record').innerHTML=r.rows.map(row=>'<p class="formula"><b>'+row.id+'</b> · Raw: '+esc(row.power)+' W, '+esc(row.duration)+' '+row.unit+' → '+display(row.hours)+' h → '+display(row.energy)+' Wh<br><small>Exact hours: '+exact(row.hours)+'; exact energy: '+exact(row.energy)+' Wh.</small></p>').join('');put('sum','Unrounded sum: '+r.rows.map(row=>exact(row.energy)).join(' + ')+' = '+exact(r.total)+' Wh.');put('status','Current total of '+rows.length+' declared interval(s); constant supplied power per row.');put('error','');}catch(e){invalidate();put('status','No current total; every required row must be valid.');put('error',e.message);}}
function onEdit(e){const id=e.target.dataset.row,field=e.target.dataset.field,row=rows.find(r=>r.id===id);if(row&&['power','duration','unit'].includes(field)){row[field]=e.target.value;invalidate();}}
$('rows').addEventListener('input',onEdit);$('rows').addEventListener('change',onEdit);$('rows').addEventListener('click',e=>{const id=e.target.dataset.remove;if(!id||rows.length<=1)return;rows=rows.filter(r=>r.id!==id);renderRows();invalidate();$('power-'+rows[0].id).focus()});$('add').addEventListener('click',()=>{if(rows.length>=10)return;const id='E'+String(nextId++).padStart(2,'0');rows.push({id,power:'',duration:'',unit:''});renderRows();invalidate();$('power-'+id).focus()});$('calculate').addEventListener('click',calculate);$('reset').addEventListener('click',()=>{rows=[{id:'E01',power:'',duration:'',unit:''}];nextId=2;renderRows();invalidate();put('status','');$('power-E01').focus()});function fixture(data){rows=data.map((r,i)=>({id:'E'+String(i+1).padStart(2,'0'),power:r[0],duration:r[1],unit:r[2]}));nextId=rows.length+1;renderRows();calculate()};$('minutes').addEventListener('click',()=>fixture([['30','90','min']]));$('seconds').addEventListener('click',()=>fixture([['30','5400','s']]));$('segments').addEventListener('click',()=>fixture([['30','1','h'],['10','2','h']]));globalThis.Tool={calculateRows,calculate,fixture,getRows:()=>rows.map(r=>({...r})),exact};renderRows();calculate();</script></body></html>