{
  "schemaVersion": 1,
  "slug": "character-counter-calculator",
  "category": "productivity",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "80bef0c3f07fb40879753abcac746f3774db5e588b6302c64615835ba6cf90fe",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/character-counter-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": "N_SMS = GSM7 units/160 or 153; Unicode UTF-16 units/70 or 67",
    "vars": [
      [
        "n",
        "Characters (trimmed)"
      ],
      [
        "L",
        "Platform character limit"
      ],
      [
        "R",
        "Characters remaining"
      ],
      [
        "N_SMS",
        "SMS segments"
      ]
    ],
    "note": "Grapheme clusters count visible characters. SMS uses GSM-7 (160/153 units) or Unicode UTF-16 (70/67 units); platform presets are reference values."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const L={twitter:280,meta:155,title:60,instagram:2200,linkedin:3000,sms:160,custom:targetLimit}; const raw=String(text||''); const trimmed=raw.trim(); const graphemes=s=>typeof Intl.Segmenter==='function'?Array.from(new Intl.Segmenter('und',{granularity:'grapheme'}).segment(s)):Array.from(s); const characters=raw?graphemes(raw).length:Math.round(words*5.1); const charactersNoSpaces=raw?graphemes(raw.replace(/\\s/g,'')).length:Math.round(words*4.1); const script=/[\\u3040-\\u30ff]/.test(trimmed)?'ja':/[\\uac00-\\ud7af]/.test(trimmed)?'ko':/[\\u0e00-\\u0e7f]/.test(trimmed)?'th':/[\\u3400-\\u9fff]/.test(trimmed)?'zh':null; const wordsCount=trimmed?(script&&typeof Intl.Segmenter==='function'?Array.from(new Intl.Segmenter(script,{granularity:'word'}).segment(trimmed)).filter(x=>x.isWordLike).length:trimmed.split(/\\s+/).filter(Boolean).length):words; const gsmBasic='@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞ !\\\"#¤%&\\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ§¿abcdefghijklmnopqrstuvwxyzäöñüà'; const gsmExt='\\f^{}\\\\[~]|€'; const smsText=raw||' '.repeat(Math.max(0,Math.round(words*5.1))); let isGsm=true,septets=0; for(const ch of smsText){if(gsmBasic.includes(ch))septets+=1;else if(gsmExt.includes(ch))septets+=2;else{isGsm=false;break;}} const smsUnits=isGsm?septets:smsText.length; const smsLimit=isGsm?160:70; const limit=platform==='sms'?smsLimit:(L[platform]||targetLimit); const remaining=platform==='sms'?smsLimit-smsUnits:limit-characters; const overage=Math.abs(remaining); const status=remaining>=0?remaining>limit*.3?'Comfortably within the limit':'Fits, but close to the limit':platform==='sms'?'Over limit by '+overage+' '+(overage===1?'unit':'units'):'Over by '+overage+' characters'; const smsSegments=smsUnits===0?0:smsUnits<=smsLimit?1:Math.ceil(smsUnits/(isGsm?153:67)); return {characters,charactersNoSpaces,words:wordsCount,limit,remaining,status,smsSegments};",
    "inputs": [
      {
        "name": "text",
        "type": "text",
        "default": "",
        "step": 1
      },
      {
        "name": "targetLimit",
        "type": "number",
        "default": 280,
        "min": 1,
        "max": 100000,
        "step": 1
      },
      {
        "name": "platform",
        "type": "select",
        "default": "twitter",
        "options": [
          {
            "value": "twitter"
          },
          {
            "value": "meta"
          },
          {
            "value": "title"
          },
          {
            "value": "instagram"
          },
          {
            "value": "linkedin"
          },
          {
            "value": "sms"
          },
          {
            "value": "custom"
          }
        ]
      },
      {
        "name": "words",
        "type": "number",
        "default": 50,
        "min": 0,
        "max": 100000,
        "step": 1
      }
    ],
    "outputs": [
      {
        "name": "characters",
        "precision": 0,
        "unit": "chars"
      },
      {
        "name": "charactersNoSpaces",
        "precision": 0,
        "unit": "chars"
      },
      {
        "name": "words",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "limit",
        "precision": 0,
        "unit": "chars"
      },
      {
        "name": "remaining",
        "precision": 0,
        "unit": "chars"
      },
      {
        "name": "status",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "smsSegments",
        "precision": 0,
        "unit": ""
      }
    ],
    "interpret": null
  }
}
