{
  "schemaVersion": 1,
  "slug": "tip-calculator",
  "category": "everyday",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "3af7c06333e4060d98219ead8d681cb7d81b0f445c3ef364fe0e4fa48219df94",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/tip-calculator.json",
    "src/engine.js"
  ],
  "runtimeSource": "/sources/engine.txt",
  "runtimeHash": "5c888d8898afbdee36267bb9977829e1e2ac568f6677c8ee87469c139864645c",
  "scope": "The compute function records implemented equations, constants, lookup tables and custom rules. Inputs record defaults and allowed values; outputs record precision and units. Defaults are examples, not recommendations. General background references are not evidence for this specific algorithm.",
  "publishedFormula": {
    "formula": "tax = B × t/100; tip = basis × p/100; raw total = B + tax + tip; exact share = raw total/N; extra = collected − raw total",
    "vars": [],
    "note": "Rounding the whole bill and rounding every share are different: the collected total and extra collected are shown separately. Country reminders provide context and do not set a tip rate."
  },
  "externalEvidence": {
    "reviewedAt": "2026-10-31",
    "sources": [
      {
        "name": "IRS — Tip and service-charge definitions",
        "url": "https://www.irs.gov/businesses/small-businesses-self-employed/tip-recordkeeping-and-reporting"
      },
      {
        "name": "OpenStax Prealgebra 2e — Solve percent problems",
        "url": "https://openstax.org/books/prealgebra-2e/pages/6-2-solve-general-applications-of-percent"
      }
    ],
    "referenceRole": "context",
    "implementationHash": "3af7c06333e4060d98219ead8d681cb7d81b0f445c3ef364fe0e4fa48219df94",
    "basis": "Enter the subtotal in one currency. Tax is added once. Choose a tip on the subtotal or subtotal plus tax; included service sets the extra tip to zero. Rounding the whole bill and rounding every share are different: the collected total and extra collected are shown separately. Country reminders provide context and do not set a tip rate.",
    "limitations": "Rounding the whole bill and rounding every share are different: the collected total and extra collected are shown separately. Country reminders provide context and do not set a tip rate."
  },
  "contract": {
    "compute": "try{if(((true)&&(!Number.isFinite(bill)||bill<0))||((true)&&(!Number.isFinite(tipPercent)||tipPercent<0||tipPercent>100))||((true)&&(!Number.isFinite(people)||people<1||people>100))||((true)&&(!Number.isFinite(taxPercent)||taxPercent<0||taxPercent>100))||((true)&&(!Number.isFinite(roundUp)||roundUp<0||roundUp>100)))throw new Error('inputError');const calculate=function(){const h=(function context() {\n  const DAY=86400000;\n  const own=(o,k)=>Object.prototype.hasOwnProperty.call(o,k);\n  const fail=()=>{throw new Error('inputError');};\n  const num=(v,min=-Infinity,max=Infinity,integer=false)=>{\n    if(!Number.isFinite(v)||v<min||v>max||(integer&&!Number.isInteger(v)))fail();return v;\n  };\n  const choice=(v,values)=>{if(!values.includes(v))fail();return v;};\n  const date=s=>{if(typeof s!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(s)||s.startsWith('0000'))fail();const d=new Date(s+'T00:00:00Z');if(!Number.isFinite(+d)||d.toISOString().slice(0,10)!==s)fail();return d;};\n  const iso=d=>{if(!Number.isFinite(+d)||d.getUTCFullYear()<1||d.getUTCFullYear()>9999)fail();return d.toISOString().slice(0,10);};\n  const shift=(d,n)=>{const r=new Date(+d+n*DAY);iso(r);return r;};\n  const month=(d,n)=>{const r=new Date(+d);r.setUTCDate(1);r.setUTCMonth(r.getUTCMonth()+n);const end=new Date(+r);end.setUTCMonth(end.getUTCMonth()+1,0);r.setUTCDate(Math.min(d.getUTCDate(),end.getUTCDate()));iso(r);return r;};\n  const weekdays=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];\n  const work=(d,count)=>{let n=Math.floor(count/7)*5;for(let k=0;k<count%7;k++){const w=(d.getUTCDay()+k)%7;if(w!==0&&w!==6)n++;}return n;};\n  const wholeMonths=(a,b)=>{let n=(b.getUTCFullYear()-a.getUTCFullYear())*12+b.getUTCMonth()-a.getUTCMonth();if(n>=0&&+month(a,n)>+b)n--;if(n<0&&+month(a,n)<+b)n++;return n;};\n  return {DAY,own,fail,num,choice,date,iso,shift,month,weekdays,work,wholeMonths};\n})();const result=(function(v,h){\n    const {num,choice}=h;num(v.bill,0,1e8);num(v.tipPercent,0,100);num(v.people,1,100,true);num(v.taxPercent,0,100);num(v.roundUp,0,100);\n    choice(v.tipOn,['preTax','postTax']);choice(v.serviceIncluded,['yes','no']);choice(v.roundTarget,['person','total']);\n    const tax=v.bill*v.taxPercent/100,tipAmount=v.serviceIncluded==='yes'?0:(v.bill+(v.tipOn==='postTax'?tax:0))*v.tipPercent/100;\n    const total=v.bill+tax+tipAmount,perPersonExact=total/v.people;\n    const up=(x,n)=>n?Math.ceil(x/n-Number.EPSILON*Math.max(1,x/n)*4)*n:x;\n    const collectedTotal=v.roundTarget==='total'?up(total,v.roundUp):up(perPersonExact,v.roundUp)*v.people;\n    const reminders={us:'Check the bill for service charges; customs vary by venue',uk:'Check whether any service charge is mandatory or discretionary',canada:'Check the bill and local custom; charges vary by venue',france:'Check the bill and local custom before adding a tip',germany:'Check the bill and local custom before adding a tip',spain:'Check the bill and local custom before adding a tip',italy:'Check the bill and local custom before adding a tip',japan:'Tipping is generally not customary; check the venue',australia:'Check the bill; tipping practices vary by venue',uae:'Check the bill for service charges before adding a tip'};\n    choice(v.country,Object.keys(reminders));\n    return {tax,tip:tipAmount,tipAmount,total,perPersonExact,perPerson:collectedTotal/v.people,overpay:Math.max(0,collectedTotal-total),collectedTotal,localNorm:reminders[v.country],advice:v.serviceIncluded==='yes'?'Service is already on the bill — an extra tip is optional':'Check the bill and local custom before adding a tip'};\n  })({\"bill\":(bill),\"tipPercent\":(tipPercent),\"people\":(people),\"taxPercent\":(taxPercent),\"tipOn\":(tipOn),\"serviceIncluded\":(serviceIncluded),\"roundUp\":(roundUp),\"roundTarget\":(roundTarget),\"country\":(country)},h);return result;};const out=calculate();return Object.fromEntries([\"perPerson\",\"total\",\"tip\",\"tax\",\"perPersonExact\",\"overpay\",\"collectedTotal\",\"localNorm\",\"advice\"].map(k=>[k,out[k]??null]).concat(Object.entries(out).filter(([k])=>k.startsWith('__'))));}catch(error){return Object.assign(Object.fromEntries([\"perPerson\",\"total\",\"tip\",\"tax\",\"perPersonExact\",\"overpay\",\"collectedTotal\",\"localNorm\",\"advice\"].map(k=>[k,null])),{__error:error.message==='openingsExceedWallArea'?'openingsExceedWallArea':'inputError'});}",
    "inputs": [
      {
        "name": "bill",
        "type": "number",
        "default": 80,
        "min": 0,
        "step": 0.01,
        "strictRange": true,
        "requiredValue": true
      },
      {
        "name": "tipPercent",
        "type": "number",
        "default": 15,
        "min": 0,
        "max": 100,
        "step": 0.01,
        "strictRange": true,
        "requiredValue": true,
        "advanced": false
      },
      {
        "name": "people",
        "type": "number",
        "default": 2,
        "min": 1,
        "max": 100,
        "step": 1,
        "slider": true,
        "strictRange": true,
        "requiredValue": true
      },
      {
        "type": "number",
        "default": 0,
        "min": 0,
        "max": 100,
        "step": 0.01,
        "strictRange": true,
        "requiredValue": true,
        "advanced": true,
        "name": "taxPercent"
      },
      {
        "type": "select",
        "default": "preTax",
        "options": [
          {
            "value": "preTax"
          },
          {
            "value": "postTax"
          }
        ],
        "advanced": true,
        "name": "tipOn"
      },
      {
        "type": "select",
        "default": "no",
        "options": [
          {
            "value": "no"
          },
          {
            "value": "yes"
          }
        ],
        "advanced": true,
        "name": "serviceIncluded"
      },
      {
        "type": "number",
        "default": 0,
        "min": 0,
        "max": 100,
        "step": 0.01,
        "strictRange": true,
        "requiredValue": true,
        "advanced": true,
        "name": "roundUp"
      },
      {
        "type": "select",
        "default": "person",
        "options": [
          {
            "value": "person"
          },
          {
            "value": "total"
          }
        ],
        "advanced": true,
        "name": "roundTarget"
      },
      {
        "name": "country",
        "type": "select",
        "default": "us",
        "options": [
          {
            "value": "us"
          },
          {
            "value": "uk"
          },
          {
            "value": "canada"
          },
          {
            "value": "france"
          },
          {
            "value": "germany"
          },
          {
            "value": "spain"
          },
          {
            "value": "italy"
          },
          {
            "value": "japan"
          },
          {
            "value": "australia"
          },
          {
            "value": "uae"
          }
        ],
        "advanced": true
      }
    ],
    "outputs": [
      {
        "name": "perPerson",
        "precision": 2,
        "unit": "USD",
        "notation": "auto"
      },
      {
        "name": "total",
        "precision": 2,
        "unit": "USD",
        "notation": "auto"
      },
      {
        "name": "tip",
        "precision": 2,
        "unit": "USD",
        "notation": "auto"
      },
      {
        "name": "tax",
        "precision": 2,
        "unit": "$",
        "notation": "auto"
      },
      {
        "name": "perPersonExact",
        "precision": 4,
        "unit": "$",
        "notation": "auto"
      },
      {
        "name": "overpay",
        "precision": 2,
        "unit": "$",
        "notation": "auto"
      },
      {
        "precision": 2,
        "unit": "$",
        "name": "collectedTotal",
        "notation": "auto"
      },
      {
        "name": "localNorm",
        "precision": 0,
        "unit": "",
        "type": "text",
        "optional": true
      },
      {
        "name": "advice",
        "precision": 0,
        "unit": "",
        "type": "text",
        "optional": true
      }
    ],
    "interpret": null
  }
}
