{
  "schemaVersion": 1,
  "slug": "sunlight-hours-calculator",
  "category": "gardening",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "1b89fb7d0660c034a2ea826596e7d62ecb9f245e4c852cd3d4456c89262ad533",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/sunlight-hours-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": "δ = −23.44° cos(2π(n+10)÷365); H = 2 acos(−tan φ tan δ) ÷ 15",
    "vars": [
      [
        "n",
        "Day of year"
      ],
      [
        "φ",
        "Latitude (degrees)"
      ],
      [
        "δ",
        "Solar declination"
      ],
      [
        "H",
        "Day length (hours)"
      ]
    ],
    "note": "Day length H uses a simplified solar-declination estimate; polar day/night are capped at 24/0 h. Usable sun = max(0, H − entered shade hours); no extra fixed deduction is used. Clock offset shifts displayed clock times only."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const validDate = (s) => { const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(String(s)); if (!m) return false; const d = new Date(String(s) + 'T00:00:00Z'); return Number.isFinite(d.getTime()) && d.getUTCFullYear() === +m[1] && d.getUTCMonth()+1 === +m[2] && d.getUTCDate() === +m[3]; };\nif (!validDate(date)) return { dayLengthHours: null, sunriseTime: null, sunsetTime: null, solarNoonTime: null, changeMinutesPerDay: null, usableSunHours: null, seasonLabel: null, note: null, __error: 'inputError' };\nconst d = new Date(date + 'T00:00:00Z');\nconst dayOfYear = Math.floor((Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) - Date.UTC(d.getUTCFullYear(), 0, 0))/86400000);\nconst lengthFor = function(n){\n  const decl = -23.44*Math.cos(2*Math.PI/365*(n + 10))*Math.PI/180;\n  const lat = latitude*Math.PI/180;\n  const cosH = -Math.tan(lat)*Math.tan(decl);\n  if (cosH <= -1) return 24;\n  if (cosH >= 1) return 0;\n  return 2*Math.acos(cosH)*180/Math.PI/15;\n};\nconst hours = lengthFor(dayOfYear);\nconst tomorrow = lengthFor(dayOfYear + 1);\nconst fmt = function(h){\n  const x = ((h % 24) + 24) % 24;\n  let hh = Math.floor(x);\n  let mm = Math.round((x - hh)*60);\n  if (mm === 60) { hh = (hh + 1) % 24; mm = 0; }\n  return String(hh).padStart(2, '0') + ':' + String(mm).padStart(2, '0');\n};\nconst sunrise = 12 - hours/2 + clockOffsetHours;\nconst sunset = 12 + hours/2 + clockOffsetHours;\nconst change = (tomorrow - hours)*60;\nconst season = hours >= 16 ? 'Longer daylight'\n  : hours >= 12.5 ? 'Moderately long daylight'\n  : hours >= 11.5 ? 'Near-equal day and night'\n  : hours >= 9 ? 'Moderately short daylight'\n  : 'Shorter daylight';\nconst usable = Math.max(0, hours - shadeHours);\nconst note = hours >= 24 ? 'The sun does not set at this latitude on this date.'\n  : hours <= 0 ? 'The sun does not rise at this latitude on this date.'\n  : change > 0 ? 'Daylight is lengthening by about ' + change.toFixed(1) + ' minutes a day.'\n  : 'Daylight is shortening by about ' + Math.abs(change).toFixed(1) + ' minutes a day.';\nreturn { dayLengthHours: hours, sunriseTime: fmt(sunrise), sunsetTime: fmt(sunset), solarNoonTime: fmt(12 + clockOffsetHours), changeMinutesPerDay: change, usableSunHours: usable, seasonLabel: season, note: note };",
    "inputs": [
      {
        "name": "latitude",
        "type": "number",
        "default": 40,
        "min": -90,
        "max": 90,
        "step": 0.5,
        "slider": true
      },
      {
        "name": "date",
        "type": "date",
        "default": "@today",
        "step": 1
      },
      {
        "name": "clockOffsetHours",
        "type": "number",
        "default": 0,
        "min": -13,
        "max": 14,
        "step": 0.25,
        "slider": true
      },
      {
        "name": "shadeHours",
        "type": "number",
        "default": 0,
        "min": 0,
        "max": 12,
        "step": 0.5,
        "slider": true
      }
    ],
    "outputs": [
      {
        "name": "dayLengthHours",
        "precision": 2,
        "unit": "hours"
      },
      {
        "name": "sunriseTime",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "sunsetTime",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "solarNoonTime",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "changeMinutesPerDay",
        "precision": 1,
        "unit": "min"
      },
      {
        "name": "usableSunHours",
        "precision": 2,
        "unit": "hours"
      },
      {
        "name": "seasonLabel",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "note",
        "precision": 0,
        "unit": "",
        "type": "text"
      }
    ],
    "interpret": null
  }
}
