[{"data":1,"prerenderedAt":416},["ShallowReactive",2],{"navigation_docs":3,"-design-adrs-002-set-primary-spans":101,"-design-adrs-002-set-primary-spans-surround":413},[4,23,66],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":22},"Guide","i-lucide-book-open","\u002Fguide","1.guide",[10,14,18],{"title":11,"path":12,"stem":13},"Introduction","\u002Fguide\u002Fintroduction","1.guide\u002F1.introduction",{"title":15,"path":16,"stem":17},"Getting started","\u002Fguide\u002Fgetting-started","1.guide\u002F2.getting-started",{"title":19,"path":20,"stem":21},"Pieces and sets","\u002Fguide\u002Fpieces-and-sets","1.guide\u002F3.pieces-and-sets",false,{"title":24,"icon":25,"path":26,"stem":27,"children":28},"Design (ADRs)","i-lucide-drafting-compass","\u002Fdesign","2.design",[29,32],{"title":30,"path":26,"stem":31},"Design decisions (ADRs)","2.design\u002Findex",{"title":33,"icon":34,"path":35,"stem":36,"children":37,"page":22},"Decision records","i-lucide-scroll-text","\u002Fdesign\u002Fadrs","2.design\u002Fadrs",[38,42,46,50,54,58,62],{"title":39,"path":40,"stem":41},"ADR-001: The refinement-chain model for calendar points","\u002Fdesign\u002Fadrs\u002F001-refinement-chain","2.design\u002Fadrs\u002F001-refinement-chain",{"title":43,"path":44,"stem":45},"ADR-002: Sets first — the Span algebra","\u002Fdesign\u002Fadrs\u002F002-set-primary-spans","2.design\u002Fadrs\u002F002-set-primary-spans",{"title":47,"path":48,"stem":49},"ADR-003: Overflow policy — clamp by default, skip on request","\u002Fdesign\u002Fadrs\u002F003-overflow-policy","2.design\u002Fadrs\u002F003-overflow-policy",{"title":51,"path":52,"stem":53},"ADR-004: Fortnights as every-k scopes","\u002Fdesign\u002Fadrs\u002F004-fortnight-scopes","2.design\u002Fadrs\u002F004-fortnight-scopes",{"title":55,"path":56,"stem":57},"ADR-005: One denotation function, and what DST does to it","\u002Fdesign\u002Fadrs\u002F005-shared-denotation","2.design\u002Fadrs\u002F005-shared-denotation",{"title":59,"path":60,"stem":61},"ADR-006: Zocam is a standalone, released library","\u002Fdesign\u002Fadrs\u002F006-standalone-library","2.design\u002Fadrs\u002F006-standalone-library",{"title":63,"path":64,"stem":65},"ADR-007: A set is one value — struct answers, enumerable shapes","\u002Fdesign\u002Fadrs\u002F007-one-value-sets","2.design\u002Fadrs\u002F007-one-value-sets",{"title":67,"path":68,"stem":69,"children":70,"icon":100},"API Reference","\u002Fapi","3.api\u002Findex",[71,72,76,80,84,88,92,96],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75},"Zocam","\u002Fapi\u002Fzocam","3.api\u002Fzocam",{"title":77,"path":78,"stem":79},"Zocam.Intervals","\u002Fapi\u002Fzocam-intervals","3.api\u002Fzocam-intervals",{"title":81,"path":82,"stem":83},"Zocam.ISO","\u002Fapi\u002Fzocam-iso","3.api\u002Fzocam-iso",{"title":85,"path":86,"stem":87},"Zocam.Point","\u002Fapi\u002Fzocam-point","3.api\u002Fzocam-point",{"title":89,"path":90,"stem":91},"Zocam.Point.ComposeError","\u002Fapi\u002Fzocam-point-composeerror","3.api\u002Fzocam-point-composeerror",{"title":93,"path":94,"stem":95},"Zocam.Span","\u002Fapi\u002Fzocam-span","3.api\u002Fzocam-span",{"title":97,"path":98,"stem":99},"Zocam.Span.Arc","\u002Fapi\u002Fzocam-span-arc","3.api\u002Fzocam-span-arc","i-lucide-braces",{"id":102,"title":43,"body":103,"description":131,"extension":407,"links":408,"meta":409,"navigation":410,"path":44,"seo":411,"stem":45,"__hash__":412},"docs\u002F2.design\u002Fadrs\u002F002-set-primary-spans.md",{"type":104,"value":105,"toc":400},"minimark",[106,124,129,132,136,139,142,146,166,172,185,189,202,248,254,257,344,348,355,361,377,396],[107,108,109,115,116,123],"p",{},[110,111,114],"span",{"className":112},[113],"ai-slop","AI SLOP"," an AI agent wrote this page. ",[117,118,122],"a",{"href":119,"rel":120},"https:\u002F\u002Fgithub.com\u002Fyuri4n",[121],"nofollow","yuri4n",", a senior engineer, gave the direction and did the review. The review is human, thus errors can stay.",[125,126,128],"h2",{"id":127},"status","Status",[107,130,131],{},"Accepted, 2026-08-04, decided by the project owner.",[125,133,135],{"id":134},"context","Context",[107,137,138],{},"A single calendar point cannot express common schedules. \"15:00 of every day of May\" needs an intersection. \"Mondays and Wednesdays\" needs a union. \"Every 15 minutes from 09:00 to 17:00\" needs an interval with a step.",[107,140,141],{},"The library needs two things at once: bounded ranges over calendar points (arcs), and a set algebra over them (union, intersection, complement, diff). The force in tension is closure. The complement of one arc is already two pieces. A diff can cut one interval into two. The chosen type must survive its own operators.",[125,143,145],{"id":144},"options","Options",[107,147,148,152,153,157,158,161,162,165],{},[149,150,151],"strong",{},"Option A: interval as the primary type."," A ",[154,155,156],"code",{},"Span"," is one interval; a helper wraps lists of them. Caveat: this type is not closed. ",[154,159,160],{},"Span.complement\u002F1"," and ",[154,163,164],{},"Span.diff\u002F2"," return values the type cannot hold, so every call site handles a list-or-single special case.",[107,167,168,171],{},[149,169,170],{},"Option B: eager enumeration."," Evaluate every operator to a flat list of concrete intervals. Caveat: \"Wednesdays in May\" has no finite normal form before a horizon is fixed. Cross-cycle sets must stay symbolic.",[107,173,174,177,178,161,181,184],{},[149,175,176],{},"Option C: a recursive set type with smart constructors."," The pattern is an algebraic data type: five constructors, one recursive type. Constructors build a free algebra. ",[154,179,180],{},"member?\u002F2",[154,182,183],{},"ground\u002F3"," are its interpreters (the interpreter pattern). Caveat: structural equality is only quasi-canonical. True semantic equality needs a horizon.",[125,186,188],{"id":187},"decision","Decision",[107,190,191,192,194,195,198,199,201],{},"Option C is the chosen one. ",[154,193,93],{}," is a recursive set algebra over points and arcs. Sets are primary. A single point lifts into the algebra with ",[154,196,197],{},"Span.of\u002F1",". This keeps ",[154,200,85],{}," small and pure.",[203,204,209],"pre",{"className":205,"code":206,"language":207,"meta":208,"style":208},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","graph TD\n  S[\"Span.t\"] --> A[\"{:arcs, scope, grain, [Arc]}\"]\n  S --> U[\"{:union, [Span]}\"]\n  S --> I[\"{:intersection, [Span]}\"]\n  S --> C[\"{:complement, Span}\"]\n  S --> N[\"{:nth, n, Span, per: cycle}\"]\n","mermaid","",[154,210,211,218,224,230,236,242],{"__ignoreMap":208},[110,212,215],{"class":213,"line":214},"line",1,[110,216,217],{},"graph TD\n",[110,219,221],{"class":213,"line":220},2,[110,222,223],{},"  S[\"Span.t\"] --> A[\"{:arcs, scope, grain, [Arc]}\"]\n",[110,225,227],{"class":213,"line":226},3,[110,228,229],{},"  S --> U[\"{:union, [Span]}\"]\n",[110,231,233],{"class":213,"line":232},4,[110,234,235],{},"  S --> I[\"{:intersection, [Span]}\"]\n",[110,237,239],{"class":213,"line":238},5,[110,240,241],{},"  S --> C[\"{:complement, Span}\"]\n",[110,243,245],{"class":213,"line":244},6,[110,246,247],{},"  S --> N[\"{:nth, n, Span, per: cycle}\"]\n",[107,249,250],{},[251,252,253],"em",{},"Figure 1 — The five constructors of the recursive Span type. AI generated, human reviewed.",[107,255,256],{},"The precise rules:",[258,259,260,282,312,334],"ul",{},[261,262,263,266,267,270,271,274,275,278,279,281],"li",{},[149,264,265],{},"Canonical forms."," The empty set is ",[154,268,269],{},"{:union, []}","; the universe is ",[154,272,273],{},"{:intersection, []}",". Smart constructors rewrite toward them, so identities hold: ",[154,276,277],{},"diff(a, empty())"," returns ",[154,280,117],{},", never empty. This pair closes the algebra — call sites see no special cases.",[261,283,284,287,288,291,292,295,296,299,300,303,304,307,308,311],{},[149,285,286],{},"Arcs."," An arc canonicalizes to half-open integer cell ranges per ",[154,289,290],{},"(scope, grain)",", so compaction is integer math. A step desugars into step-1 unions ",[251,293,294],{},"before"," wrap-splitting, so sampling runs through the seam. When a wrap-around arc (",[154,297,298],{},"Fri..Mon",", ",[154,301,302],{},"22:00..06:00",") splits, both seam sides are ",[154,305,306],{},":closed",". The outer sides keep their closings. Wrap is classified at the chain level, before closings expand: ",[154,309,310],{},"May..May"," is the single-unit case, never a wrap.",[261,313,314,317,318,321,322,325,326,329,330,333],{},[149,315,316],{},"Steps."," A step is ",[154,319,320],{},"{n, unit}",". The unit must be an ancestor-or-self of the arc grain: a monthly step over day bounds is legal (\"the 31st, monthly\"). A ",[154,323,324],{},":time"," grain requires an explicit time unit (",[154,327,328],{},":hour | :minute | :second","), because ",[154,331,332],{},"Time.t"," has no natural cell size.",[261,335,336,339,340,343],{},[149,337,338],{},"Ordinals."," ",[154,341,342],{},"{:nth, n, thing, per: unit}"," is in v1, for payroll cases such as \"last working day of the month\". Inner sets are leaf-only. Grounding widens to whole scope instances, then clips, so a cut-off month never miscounts.",[125,345,347],{"id":346},"consequences","Consequences",[107,349,350,351,354],{},"Easy now: composing schedules. Every operator takes spans and returns a span. \"Many versus one\" is not a special case, because \"many\" is already a union value. ",[154,352,353],{},"Point"," stays free of set logic and timezone code.",[107,356,357,358,360],{},"Hard now: equality. Two spans can denote the same set but differ structurally. Comparing them needs ",[154,359,183],{}," over a horizon.",[107,362,363,364,366,367,370,371,373,374,376],{},"Open items: none in the code — ",[154,365,93],{}," is implemented. The tests in ",[154,368,369],{},"test\u002Fzocam\u002Fspan_test.exs"," pin the semantics above. The core property — ",[154,372,180],{}," agrees with ",[154,375,183],{}," everywhere inside the horizon — must stay guarded by tests.",[378,379,380],"blockquote",{},[107,381,382,383,389,390,161,392,395],{},"See ",[117,384,385,386,388],{"href":94},"the ",[154,387,93],{}," module reference"," for the full ",[154,391,156],{},[154,393,394],{},"Arc"," types.",[397,398,399],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":208,"searchDepth":220,"depth":220,"links":401},[402,403,404,405,406],{"id":127,"depth":220,"text":128},{"id":134,"depth":220,"text":135},{"id":144,"depth":220,"text":145},{"id":187,"depth":220,"text":188},{"id":346,"depth":220,"text":347},"md",null,{},true,{"title":43,"description":131},"dxlMzHbIBqmIWd8KsVLHAdKCPGzVqyXnKnWH9ET_z8E",[414,415],{"title":39,"path":40,"stem":41,"description":131,"children":-1},{"title":47,"path":48,"stem":49,"description":131,"children":-1},1786065169199]