[{"data":1,"prerenderedAt":1398},["ShallowReactive",2],{"navigation_docs":3,"-api-zocam-intervals":101,"-api-zocam-intervals-surround":1393},[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":77,"body":103,"description":1386,"extension":1387,"links":1388,"meta":1389,"navigation":1390,"path":78,"seo":1391,"stem":79,"__hash__":1392},"docs\u002F3.api\u002Fzocam-intervals.md",{"type":104,"value":105,"toc":1352},"minimark",[106,129,138,141,144,154,180,185,214,244,247,253,256,262,266,281,284,290,297,303,310,313,319,322,328,332,338,351,356,396,401,410,415,450,463,468,477,482,491,496,505,510,519,541,546,555,560,569,574,583,587,593,602,605,608,656,670,676,682,691,711,721,727,733,739,748,760,782,788,794,803,827,833,840,846,852,861,871,881,887,892,901,920,926,929,935,941,950,964,970,975,984,1006,1009,1015,1021,1027,1032,1041,1058,1063,1066,1072,1075,1081,1086,1092,1097,1106,1119,1125,1130,1136,1141,1150,1164,1170,1175,1180,1186,1195,1198,1207,1213,1219,1225,1232,1238,1244,1250,1256,1265,1268,1282,1288,1293,1302,1308,1314,1317,1323,1329,1335,1338,1348],[107,108,109,115,116,120,121,128],"p",{},[110,111,114],"span",{"className":112},[113],"ai-slop","AI SLOP"," An AI agent wrote the docstrings; ",[117,118,119],"code",{},"mix docs"," generated this page. ",[122,123,127],"a",{"href":124,"rel":125},"https:\u002F\u002Fgithub.com\u002Fyuri4n",[126],"nofollow","yuri4n",", a senior engineer, gave the direction and did the review. The review is human, thus errors can stay.",[107,130,131],{},[122,132,137],{"href":133,"rel":134,"className":135},"https:\u002F\u002Fgithub.com\u002Fyuri4n\u002Fzocam\u002Fblob\u002Fmain\u002Flib\u002Fzocam\u002Fintervals.ex#L16",[126],[136],"source-link","Source on GitHub ↗",[107,139,140],{},"The linear kernel: sets of concrete intervals on one axis, with\nunion, intersection, complement, and difference.",[107,142,143],{},"An interval is a plain map with four keys:",[145,146,151],"pre",{"className":147,"code":149,"language":150},[148],"language-text","%{from: a, until: b, left: :closed, right: :open}\n\n from                       until\n  │\u003C────────── span ──────────>│\n  ●────────────────────────────○\n  left: :closed                right: :open\n  (a is inside)                (b is outside)\n","text",[117,152,149],{"__ignoreMap":153},"",[107,155,156,157,160,161,164,165,168,169,172,173,176,177,179],{},"A ",[117,158,159],{},"nil"," endpoint means \"unbounded on this side\": ",[117,162,163],{},"%{from: x, until: nil, ...}"," is the ray from ",[117,166,167],{},"x"," onward. A set of intervals\nis the ",[117,170,171],{},"%Zocam.Intervals{}"," struct; ",[117,174,175],{},"compress\u002F1"," keeps its list\nsorted and free of overlaps, and the bare literal\n",[117,178,171],{}," is the empty set.",[181,182,184],"h2",{"id":183},"one-rule-for-shapes","One rule for shapes",[107,186,187,188,191,192,191,195,198,199,202,203,205,206,209,210,213],{},"An operation on sets answers with a set; a piece constructor\nanswers with a piece. ",[117,189,190],{},"union\u002F2",", ",[117,193,194],{},"intersect\u002F2",[117,196,197],{},"diff\u002F2",",\n",[117,200,201],{},"complement\u002F1",", and ",[117,204,175],{}," accept a piece (a map or an\noption list), a list of pieces, or the struct — and always answer\nwith the struct. ",[117,207,208],{},"new!\u002F1"," builds one piece and answers with the\npiece. ",[117,211,212],{},"overlaps?\u002F2"," asks a question and answers with a boolean.",[107,215,216,217,220,221,224,225,227,228,230,231,234,235,237,238,202,240,243],{},"Endpoints are ",[117,218,219],{},"Time"," (a daily wall window) or ",[117,222,223],{},"DateTime"," (a\nconcrete window). The two kinds do not mix: not inside one\ninterval, and not inside one operation — the kernel cannot compare\na ",[117,226,219],{}," with a ",[117,229,223],{},", so every door rejects the mix with an\n",[117,232,233],{},"ArgumentError"," that names the way out (ground the daily window\nfirst, then operate on two ",[117,236,223],{}," sets). Abstract calendar\nvalues (\"a Saturday\", \"May\") do not live here: they belong to\n",[117,239,85],{},[117,241,242],{},"Zocam.Span.ground\u002F3"," turns them into the\nconcrete intervals of this module.",[107,245,246],{},"This module is the bottom layer of the library:",[145,248,251],{"className":249,"code":250,"language":150},[148],"Zocam.Point ──▶ Zocam.Span ──▶ Zocam.Intervals  (this module)\n",[117,252,250],{"__ignoreMap":153},[107,254,255],{},"A grounded span lands here, so the kernel operations compose with\nthe calendar layers. Cut a busy morning out of a grounded\nWednesday (Jan 7 2026 is a Wednesday):",[145,257,260],{"className":258,"code":259,"language":150},[148],"iex> weds = Zocam.Span.of(Zocam.Point.weekday(:wednesday))\niex> horizon = %{from: ~U[2026-01-05 00:00:00Z], until: ~U[2026-01-12 00:00:00Z], left: :closed, right: :open}\niex> week = Zocam.Span.ground(weds, horizon, \"Etc\u002FUTC\")\niex> busy = [%{from: ~U[2026-01-07 00:00:00Z], until: ~U[2026-01-07 12:00:00Z], left: :closed, right: :open}]\niex> Zocam.Intervals.diff(week, busy).intervals\n[%{from: ~U[2026-01-07 12:00:00Z], until: ~U[2026-01-08 00:00:00Z], left: :closed, right: :open}]\n",[117,261,259],{"__ignoreMap":153},[181,263,265],{"id":264},"enumerate-a-set","Enumerate a set",[107,267,268,269,272,273,276,277,280],{},"A set is one value that holds pieces, so ",[117,270,271],{},"Enum"," walks it: the\nstruct implements ",[117,274,275],{},"Enumerable"," and yields its concrete\n",[117,278,279],{},"interval\u002F0"," pieces, in normal-form order.",[107,282,283],{},"The common call — read one field off every piece:",[145,285,288],{"className":286,"code":287,"language":150},[148],"iex> free = Zocam.Intervals.diff(\n...>   Zocam.Intervals.new!(from: ~T[08:00:00], until: ~T[18:00:00]),\n...>   [%{from: ~T[09:00:00], until: ~T[10:00:00], left: :closed, right: :open}]\n...> )\niex> Enum.map(free, & &1.from)\n[~T[08:00:00], ~T[10:00:00]]\n",[117,289,287],{"__ignoreMap":153},[107,291,292,293,296],{},"The pitfall: ",[117,294,295],{},"Enum.member?\u002F2"," asks \"is this value one of the\npieces in the list?\" — it does NOT ask \"is this instant covered by\nthe set?\". An instant inside a piece is still not a piece:",[145,298,301],{"className":299,"code":300,"language":150},[148],"iex> jan7 = Zocam.Intervals.compress([\n...>   [from: ~U[2026-01-07 00:00:00Z], until: ~U[2026-01-08 00:00:00Z]]\n...> ])\niex> Enum.member?(jan7, ~U[2026-01-07 12:00:00Z])\nfalse\n",[117,302,300],{"__ignoreMap":153},[107,304,305,306,309],{},"To ask about coverage, keep the span and ask ",[117,307,308],{},"Zocam.Span.member?\u002F2",".",[107,311,312],{},"The edge: the empty set enumerates to nothing.",[145,314,317],{"className":315,"code":316,"language":150},[148],"iex> Enum.to_list(%Zocam.Intervals{})\n[]\n",[117,318,316],{"__ignoreMap":153},[107,320,321],{},"The integrated use — ground a span, then walk the result:",[145,323,326],{"className":324,"code":325,"language":150},[148],"iex> weds = Zocam.Span.of(Zocam.Point.weekday(:wednesday))\niex> horizon = %{from: ~U[2026-01-05 00:00:00Z], until: ~U[2026-01-19 00:00:00Z], left: :closed, right: :open}\niex> Zocam.Span.ground(weds, horizon, \"Etc\u002FUTC\") |> Enum.map(& &1.from)\n[~U[2026-01-07 00:00:00Z], ~U[2026-01-14 00:00:00Z]]\n",[117,327,325],{"__ignoreMap":153},[181,329,331],{"id":330},"types","Types",[333,334,336],"h3",{"id":335},"at_least_one_valid",[117,337,335],{},[145,339,343],{"className":340,"code":341,"language":342,"meta":153,"style":153},"language-elixir shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","@type at_least_one_valid() :: valid_interval() | valid_intervals()\n","elixir",[117,344,345],{"__ignoreMap":153},[110,346,349],{"class":347,"line":348},"line",1,[110,350,341],{},[333,352,354],{"id":353},"both",[117,355,353],{},[145,357,359],{"className":340,"code":358,"language":342,"meta":153,"style":153},"@type both() :: [\n  from: timables(),\n  until: timables(),\n  left: closing(),\n  right: closing()\n]\n",[117,360,361,366,372,378,384,390],{"__ignoreMap":153},[110,362,363],{"class":347,"line":348},[110,364,365],{},"@type both() :: [\n",[110,367,369],{"class":347,"line":368},2,[110,370,371],{},"  from: timables(),\n",[110,373,375],{"class":347,"line":374},3,[110,376,377],{},"  until: timables(),\n",[110,379,381],{"class":347,"line":380},4,[110,382,383],{},"  left: closing(),\n",[110,385,387],{"class":347,"line":386},5,[110,388,389],{},"  right: closing()\n",[110,391,393],{"class":347,"line":392},6,[110,394,395],{},"]\n",[333,397,399],{"id":398},"closing",[117,400,398],{},[145,402,404],{"className":340,"code":403,"language":342,"meta":153,"style":153},"@type closing() :: :open | :closed\n",[117,405,406],{"__ignoreMap":153},[110,407,408],{"class":347,"line":348},[110,409,403],{},[333,411,413],{"id":412},"interval",[117,414,412],{},[145,416,418],{"className":340,"code":417,"language":342,"meta":153,"style":153},"@type interval() :: %{\n  from: timables() | nil,\n  until: timables() | nil,\n  left: closing() | nil,\n  right: closing() | nil\n}\n",[117,419,420,425,430,435,440,445],{"__ignoreMap":153},[110,421,422],{"class":347,"line":348},[110,423,424],{},"@type interval() :: %{\n",[110,426,427],{"class":347,"line":368},[110,428,429],{},"  from: timables() | nil,\n",[110,431,432],{"class":347,"line":374},[110,433,434],{},"  until: timables() | nil,\n",[110,436,437],{"class":347,"line":380},[110,438,439],{},"  left: closing() | nil,\n",[110,441,442],{"class":347,"line":386},[110,443,444],{},"  right: closing() | nil\n",[110,446,447],{"class":347,"line":392},[110,448,449],{},"}\n",[107,451,452,453,455,456,460,461,309],{},"One concrete piece of the timeline: a plain map with exactly four\nkeys. It happens once — nothing repeats here. A ",[117,454,159],{}," endpoint\nmeans \"unbounded on this side\" (a ray). The symbolic counterpart\nthat ",[457,458,459],"em",{},"does"," repeat is ",[117,462,97],{},[333,464,466],{"id":465},"interval_opts",[117,467,465],{},[145,469,471],{"className":340,"code":470,"language":342,"meta":153,"style":153},"@type interval_opts() :: only_from() | only_until() | both()\n",[117,472,473],{"__ignoreMap":153},[110,474,475],{"class":347,"line":348},[110,476,470],{},[333,478,480],{"id":479},"only_from",[117,481,479],{},[145,483,485],{"className":340,"code":484,"language":342,"meta":153,"style":153},"@type only_from() :: [from: timables(), left: closing(), right: closing()]\n",[117,486,487],{"__ignoreMap":153},[110,488,489],{"class":347,"line":348},[110,490,484],{},[333,492,494],{"id":493},"only_until",[117,495,493],{},[145,497,499],{"className":340,"code":498,"language":342,"meta":153,"style":153},"@type only_until() :: [until: timables(), left: closing(), right: closing()]\n",[117,500,501],{"__ignoreMap":153},[110,502,503],{"class":347,"line":348},[110,504,498],{},[333,506,508],{"id":507},"t",[117,509,507],{},[145,511,513],{"className":340,"code":512,"language":342,"meta":153,"style":153},"@type t() :: %Zocam.Intervals{intervals: [interval()]}\n",[117,514,515],{"__ignoreMap":153},[110,516,517],{"class":347,"line":348},[110,518,512],{},[107,520,521,522,525,526,528,529,531,532,534,535,537,538,309],{},"A concrete ",[457,523,524],{},"set"," of pieces: the struct wraps one list of\n",[117,527,279],{}," maps, which ",[117,530,175],{}," keeps sorted and disjoint.\nAn empty list inside is the empty set, and so is the bare\nliteral ",[117,533,171],{},". This is what ",[117,536,242],{},"\nreturns. The symbolic counterpart is ",[117,539,540],{},"t:Zocam.Span.t\u002F0",[333,542,544],{"id":543},"timables",[117,545,543],{},[145,547,549],{"className":340,"code":548,"language":342,"meta":153,"style":153},"@type timables() :: Time.t() | DateTime.t()\n",[117,550,551],{"__ignoreMap":153},[110,552,553],{"class":347,"line":348},[110,554,548],{},[333,556,558],{"id":557},"valid_interval",[117,559,557],{},[145,561,563],{"className":340,"code":562,"language":342,"meta":153,"style":153},"@type valid_interval() :: interval_opts() | interval()\n",[117,564,565],{"__ignoreMap":153},[110,566,567],{"class":347,"line":348},[110,568,562],{},[333,570,572],{"id":571},"valid_intervals",[117,573,571],{},[145,575,577],{"className":340,"code":576,"language":342,"meta":153,"style":153},"@type valid_intervals() :: [interval() | interval_opts()] | t()\n",[117,578,579],{"__ignoreMap":153},[110,580,581],{"class":347,"line":348},[110,582,576],{},[181,584,586],{"id":585},"functions","Functions",[333,588,590],{"id":589},"check_and_extract_as_list",[117,591,592],{},"check_and_extract_as_list!",[145,594,596],{"className":340,"code":595,"language":342,"meta":153,"style":153},"@spec check_and_extract_as_list!(at_least_one_valid()) :: [interval()]\n",[117,597,598],{"__ignoreMap":153},[110,599,600],{"class":347,"line":348},[110,601,595],{},[107,603,604],{},"Normalize one operand into a plain list of interval maps. This is\nthe funnel: every set operation pours its operands through it, so\neach operation handles exactly ONE shape, and every bad shape is\nstopped in exactly one place.",[107,606,607],{},"The funnel accepts exactly these shapes:",[609,610,611,617,637,653],"ul",{},[612,613,614,616],"li",{},[117,615,171],{}," — a set; its pieces come out as the list.",[612,618,619,620,191,623,191,626,629,630,633,634,636],{},"One interval map with the four keys ",[117,621,622],{},":from",[117,624,625],{},":until",[117,627,628],{},":left",",\nand ",[117,631,632],{},":right"," — what ",[117,635,208],{}," builds.",[612,638,639,640,191,643,198,646,191,649,652],{},"One keyword list of interval options (",[117,641,642],{},"from:",[117,644,645],{},"until:",[117,647,648],{},"left:",[117,650,651],{},"right:",").",[612,654,655],{},"A list — empty, or holding interval maps and option lists in\nany mix.",[107,657,658,659,661,662,665,666,669],{},"Every other shape raises an ",[117,660,233],{}," that names the\nrejected shape and the accepted ones. A map with only some of the\nfour keys is such a shape: it looks like an interval and is not.\nA map with the four keys and extra keys beside them is rejected\ntoo, and so is an improper list (",[117,663,664],{},"[a | b]"," where ",[117,667,668],{},"b"," is not a\nlist): each looks like an operand and is not exactly one.",[145,671,674],{"className":672,"code":673,"language":150},[148],"iex> Zocam.Intervals.check_and_extract_as_list!(from: ~T[09:00:00], until: ~T[17:00:00])\n[%{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}]\n\niex> Zocam.Intervals.check_and_extract_as_list!(%Zocam.Intervals{})\n[]\n",[117,675,673],{"__ignoreMap":153},[333,677,679],{"id":678},"check_closings",[117,680,681],{},"check_closings!",[145,683,685],{"className":340,"code":684,"language":342,"meta":153,"style":153},"@spec check_closings!(interval()) :: interval()\n",[117,686,687],{"__ignoreMap":153},[110,688,689],{"class":347,"line":348},[110,690,684],{},[107,692,693,694,697,698,701,702,704,705,707,708,710],{},"Check the two closings, per side: a bounded side (endpoint\npresent) must be ",[117,695,696],{},":open"," or ",[117,699,700],{},":closed","; an unbounded side (endpoint\n",[117,703,159],{},") has no boundary, so its closing must be ",[117,706,159],{},". Returns the\ninterval unchanged; raises ",[117,709,233],{}," otherwise.",[107,712,713,714,717,718,720],{},"Why per side? A \"dead\" closing on an unbounded side describes a\nboundary that does not exist, and a bogus atom (",[117,715,716],{},"left: :half",")\nsilently behaves as ",[117,719,696],{}," in every comparison. Both are\nmistakes the caller wants to hear about at the door.",[107,722,723,724,726],{},"This check is shared: ",[117,725,93],{}," runs the same rule on its\nabsolute intervals, so the two layers cannot drift apart.",[145,728,731],{"className":729,"code":730,"language":150},[148],"iex> Zocam.Intervals.check_closings!(%{from: ~T[09:00:00], until: nil, left: :closed, right: nil})\n%{from: ~T[09:00:00], until: nil, left: :closed, right: nil}\n\niex> Zocam.Intervals.check_closings!(%{from: ~T[09:00:00], until: nil, left: :half, right: nil})\n** (ArgumentError) left: must be :open or :closed on a bounded side, nil on an unbounded side, got :half\n",[117,732,730],{"__ignoreMap":153},[333,734,736],{"id":735},"check_endpoints",[117,737,738],{},"check_endpoints!",[145,740,742],{"className":340,"code":741,"language":342,"meta":153,"style":153},"@spec check_endpoints!(interval()) :: interval()\n",[117,743,744],{"__ignoreMap":153},[110,745,746],{"class":347,"line":348},[110,747,741],{},[107,749,750,751,753,754,756,757,759],{},"Check that each non-nil endpoint is a value the kernel can\ncompare: a ",[117,752,219],{}," or a ",[117,755,223],{},". Returns the interval unchanged;\nraises ",[117,758,233],{}," naming the side and the value otherwise.",[107,761,762,763,766,767,770,771,753,774,777,778,781],{},"Why so strict? The kernel orders endpoints with ",[117,764,765],{},"Time.compare\u002F2","\nand ",[117,768,769],{},"DateTime.compare\u002F2",". A weekday atom crashes the first\ncomparison. A ",[117,772,773],{},"NaiveDateTime",[117,775,776],{},"Date"," is worse: it \"works\",\nbut equality falls back to structural ",[117,779,780],{},"==",", which misses equal\ninstants written differently — and then a real hole between two\nintervals is silently unioned away.",[145,783,786],{"className":784,"code":785,"language":150},[148],"iex> Zocam.Intervals.check_endpoints!(%{from: ~T[09:00:00], until: nil, left: :closed, right: nil})\n%{from: ~T[09:00:00], until: nil, left: :closed, right: nil}\n\niex> Zocam.Intervals.check_endpoints!(%{from: :saturday, until: nil, left: :closed, right: nil})\n** (ArgumentError) from: must be a Time, a DateTime, or nil, got :saturday. Calendar values (:saturday, :may) belong to Zocam.Point, and Zocam.Span.ground\u002F3 turns them into kernel intervals. A NaiveDateTime or a Date is rejected too: without a timezone it names no instant the kernel can compare.\n",[117,787,785],{"__ignoreMap":153},[333,789,791],{"id":790},"check_interval_opts",[117,792,793],{},"check_interval_opts!",[145,795,797],{"className":340,"code":796,"language":342,"meta":153,"style":153},"@spec check_interval_opts!(interval_opts()) :: interval_opts()\n",[117,798,799],{"__ignoreMap":153},[110,800,801],{"class":347,"line":348},[110,802,796],{},[107,804,805,806,809,810,809,813,809,816,819,820,809,822,824,825,710],{},"Check one interval option list: a proper keyword list, with\n",[117,807,808],{},"from","\u002F",[117,811,812],{},"until",[117,814,815],{},"left",[117,817,818],{},"right"," at most once each, and at least one\nof ",[117,821,808],{},[117,823,812],{}," present. Returns the options unchanged; raises\n",[117,826,233],{},[145,828,831],{"className":829,"code":830,"language":150},[148],"iex> Zocam.Intervals.check_interval_opts!(from: ~T[09:00:00], from: ~T[10:00:00])\n** (ArgumentError) Invalid options: from, until, left and right cannot be specified more than once\n",[117,832,830],{"__ignoreMap":153},[107,834,835,836,839],{},"The pitfall: a stray element between the pairs is not an option.\nThe check names it instead of failing deep inside ",[117,837,838],{},"Keyword",":",[145,841,844],{"className":842,"code":843,"language":150},[148],"iex> Zocam.Intervals.check_interval_opts!([{:from, ~T[09:00:00]}, :oops])\n** (ArgumentError) Invalid options: an interval option list holds {key, value} pairs only, and :oops is not one. Got: [{:from, ~T[09:00:00]}, :oops]\n",[117,845,843],{"__ignoreMap":153},[333,847,849],{"id":848},"check_order",[117,850,851],{},"check_order!",[145,853,855],{"className":340,"code":854,"language":342,"meta":153,"style":153},"@spec check_order!(interval()) :: interval()\n",[117,856,857],{"__ignoreMap":153},[110,858,859],{"class":347,"line":348},[110,860,854],{},[107,862,863,864,866,867,707,869,710],{},"Check that the interval runs forward: when both endpoints are\npresent, ",[117,865,808],{}," must not be strictly after ",[117,868,812],{},[117,870,233],{},[107,872,873,874,877,878,309],{},"Why so strict? An inverted interval covers no instant, but the\nordering helpers read it as \"before itself\", and every law of the\nalgebra breaks from there. The kernel is linear: a window that\ncrosses midnight is not one backward interval, it is a ",[457,875,876],{},"wrapping\narc"," — that concept lives one layer up, in ",[117,879,880],{},"Zocam.Span.arc!\u002F1",[145,882,885],{"className":883,"code":884,"language":150},[148],"iex> Zocam.Intervals.check_order!(%{from: ~T[06:00:00], until: ~T[22:00:00], left: :closed, right: :open})\n%{from: ~T[06:00:00], until: ~T[22:00:00], left: :closed, right: :open}\n\niex> Zocam.Intervals.check_order!(%{from: ~T[22:00:00], until: ~T[06:00:00], left: :closed, right: :open})\n** (ArgumentError) from ~T[22:00:00] is after until ~T[06:00:00]: an interval runs forward. For a window that crosses midnight, such as 22:00..06:00, build a wrapping arc with Zocam.Span.arc!\u002F1 instead.\n",[117,886,884],{"__ignoreMap":153},[333,888,890],{"id":889},"complement",[117,891,889],{},[145,893,895],{"className":340,"code":894,"language":342,"meta":153,"style":153},"@spec complement(at_least_one_valid()) :: t()\n",[117,896,897],{"__ignoreMap":153},[110,898,899],{"class":347,"line":348},[110,900,894],{},[107,902,903,904,907,908,911,912,915,916,919],{},"Everything outside the operand, as a set. Each boundary closing\nflips (see ",[117,905,906],{},"opposite_closing\u002F1","): the instants ",[117,909,910],{},"[a, b]"," covers are\nexactly the instants ",[117,913,914],{},"(-inf, a)"," and ",[117,917,918],{},"(b, +inf)"," miss. One bounded\ninterval complements to two rays:",[145,921,924],{"className":922,"code":923,"language":150},[148],"iex> Zocam.Intervals.complement(\n...>   %{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n...> ).intervals\n[\n  %{from: nil, until: ~T[09:00:00], left: nil, right: :open},\n  %{from: ~T[17:00:00], until: nil, left: :closed, right: nil}\n]\n",[117,925,923],{"__ignoreMap":153},[107,927,928],{},"The edge: the empty set complements to the whole timeline — one\ninterval, unbounded on both sides:",[145,930,933],{"className":931,"code":932,"language":150},[148],"iex> Zocam.Intervals.complement([])\n%Zocam.Intervals{intervals: [%{from: nil, until: nil, left: nil, right: nil}]}\n",[117,934,932],{"__ignoreMap":153},[333,936,938],{"id":937},"completely_before",[117,939,940],{},"completely_before?",[145,942,944],{"className":340,"code":943,"language":342,"meta":153,"style":153},"@spec completely_before?(interval(), interval()) :: boolean()\n",[117,945,946],{"__ignoreMap":153},[110,947,948],{"class":347,"line":348},[110,949,943],{},[107,951,952,953,956,957,960,961,963],{},"Is ",[117,954,955],{},"lhs"," entirely before ",[117,958,959],{},"rhs",", with no shared instant? Touching\nendpoints count as \"before\" only when at least one side is ",[117,962,696],{},"\n(the shared instant then belongs to at most one of them).",[145,965,968],{"className":966,"code":967,"language":150},[148],"iex> Zocam.Intervals.completely_before?(\n...>   %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :open},\n...>   %{from: ~T[12:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n...> )\ntrue\n",[117,969,967],{"__ignoreMap":153},[333,971,973],{"id":972},"compress",[117,974,972],{},[145,976,978],{"className":340,"code":977,"language":342,"meta":153,"style":153},"@spec compress(at_least_one_valid()) :: t()\n",[117,979,980],{"__ignoreMap":153},[110,981,982],{"class":347,"line":348},[110,983,977],{},[107,985,986,987,191,990,191,993,996,997,1000,1001,1003,1004,309],{},"Bring one operand into its normal form: drop every piece that\ncovers no instant (",[117,988,989],{},"[t, t)",[117,991,992],{},"(t, t)",[117,994,995],{},"(t, t]"," — only the\nsingle-point ",[117,998,999],{},"[t, t]"," survives), fuse everything that overlaps or\ntouches, then sort by the left endpoint (",[117,1002,159],{}," first: an unbounded\nleft side starts before everything). Two operands with the same\ncontent compress to the same struct, which makes normal forms\ncomparable with ",[117,1005,780],{},[107,1007,1008],{},"The edge: a zero-width piece with an open side holds nothing, so\ncompressing it alone gives the empty set:",[145,1010,1013],{"className":1011,"code":1012,"language":150},[148],"iex> Zocam.Intervals.compress([\n...>   %{from: ~T[12:00:00], until: ~T[12:00:00], left: :closed, right: :open}\n...> ])\n%Zocam.Intervals{intervals: []}\n\niex> Zocam.Intervals.compress([\n...>   %{from: ~T[13:00:00], until: ~T[17:00:00], left: :closed, right: :open},\n...>   %{from: ~T[09:00:00], until: ~T[11:00:00], left: :closed, right: :open},\n...>   %{from: ~T[10:00:00], until: ~T[13:00:00], left: :closed, right: :open}\n...> ])\n%Zocam.Intervals{intervals: [%{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}]}\n",[117,1014,1012],{"__ignoreMap":153},[107,1016,1017,1018,1020],{},"The edge: equality of instants follows the clock, not the struct.\nA different written precision is still the same instant — and here\nboth sides are ",[117,1019,696],{}," at 10:00, so the one-instant hole survives:",[145,1022,1025],{"className":1023,"code":1024,"language":150},[148],"iex> Zocam.Intervals.compress([\n...>   %{from: ~T[09:00:00], until: ~T[10:00:00], left: :open, right: :open},\n...>   %{from: ~T[10:00:00.000000], until: ~T[11:00:00], left: :open, right: :open}\n...> ]).intervals\n[\n  %{from: ~T[09:00:00], until: ~T[10:00:00], left: :open, right: :open},\n  %{from: ~T[10:00:00.000000], until: ~T[11:00:00], left: :open, right: :open}\n]\n",[117,1026,1024],{"__ignoreMap":153},[333,1028,1030],{"id":1029},"diff",[117,1031,1029],{},[145,1033,1035],{"className":340,"code":1034,"language":342,"meta":153,"style":153},"@spec diff(at_least_one_valid(), at_least_one_valid()) :: t()\n",[117,1036,1037],{"__ignoreMap":153},[110,1038,1039],{"class":347,"line":348},[110,1040,1034],{},[107,1042,1043,1044,1047,1048,1051,1052,1054,1055,1057],{},"Set difference: the instants of ",[117,1045,1046],{},"this"," that are not in ",[117,1049,1050],{},"other",",\nas a set. Computed piece by piece as ",[117,1053,1046],{}," intersected with the\ncomplement of ",[117,1056,1050],{},", so the three operations stay consistent by\nconstruction.",[1059,1060,1062],"h4",{"id":1061},"examples","Examples",[107,1064,1065],{},"The common call: cut the busy slots out of a working day. The\nsubtrahends subtract one after the other from what remains:",[145,1067,1070],{"className":1068,"code":1069,"language":150},[148],"iex> day = Zocam.Intervals.new!(from: ~T[08:00:00], until: ~T[18:00:00])\niex> busy = [\n...>   %{from: ~T[09:00:00], until: ~T[10:00:00], left: :closed, right: :open},\n...>   %{from: ~T[12:00:00], until: ~T[13:00:00], left: :closed, right: :open}\n...> ]\niex> Zocam.Intervals.diff(day, busy).intervals\n[\n  %{from: ~T[08:00:00], until: ~T[09:00:00], left: :closed, right: :open},\n  %{from: ~T[10:00:00], until: ~T[12:00:00], left: :closed, right: :open},\n  %{from: ~T[13:00:00], until: ~T[18:00:00], left: :closed, right: :open}\n]\n",[117,1071,1069],{"__ignoreMap":153},[107,1073,1074],{},"Subtracting a middle slice cuts one interval into two pieces of\none set:",[145,1076,1079],{"className":1077,"code":1078,"language":150},[148],"iex> block = %{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}\niex> Zocam.Intervals.diff(block, %{from: ~T[12:00:00], until: ~T[13:00:00], left: :closed, right: :open}).intervals\n[\n  %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :open},\n  %{from: ~T[13:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n]\n",[117,1080,1078],{"__ignoreMap":153},[107,1082,1083,1084,839],{},"The edge: \"nothing remains\" is the empty set — the struct with no\npieces, never ",[117,1085,159],{},[145,1087,1090],{"className":1088,"code":1089,"language":150},[148],"iex> block = %{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}\niex> Zocam.Intervals.diff(block, block)\n%Zocam.Intervals{intervals: []}\n",[117,1091,1089],{"__ignoreMap":153},[333,1093,1095],{"id":1094},"intersect",[117,1096,1094],{},[145,1098,1100],{"className":340,"code":1099,"language":342,"meta":153,"style":153},"@spec intersect(at_least_one_valid(), at_least_one_valid()) :: t()\n",[117,1101,1102],{"__ignoreMap":153},[110,1103,1104],{"class":347,"line":348},[110,1105,1099],{},[107,1107,1108,1109,1111,1112,1115,1116,839],{},"The instants shared by both operands, as a set. The tighter bound\nwins on each side, closing included: ",[117,1110,910],{}," meets ",[117,1113,1114],{},"(a, c)"," in\n",[117,1117,1118],{},"(a, b]",[145,1120,1123],{"className":1121,"code":1122,"language":150},[148],"iex> Zocam.Intervals.intersect(\n...>   %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :closed},\n...>   %{from: ~T[09:00:00], until: ~T[17:00:00], left: :open, right: :open}\n...> )\n%Zocam.Intervals{intervals: [%{from: ~T[09:00:00], until: ~T[12:00:00], left: :open, right: :closed}]}\n",[117,1124,1122],{"__ignoreMap":153},[107,1126,1127,1128,839],{},"The edge: two operands that share nothing meet in the empty set —\nthe struct with no pieces, never ",[117,1129,159],{},[145,1131,1134],{"className":1132,"code":1133,"language":150},[148],"iex> Zocam.Intervals.intersect(\n...>   %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :open},\n...>   %{from: ~T[14:00:00], until: ~T[15:00:00], left: :closed, right: :open}\n...> )\n%Zocam.Intervals{intervals: []}\n",[117,1135,1133],{"__ignoreMap":153},[333,1137,1139],{"id":1138},"interval_from_opts",[117,1140,1138],{},[145,1142,1144],{"className":340,"code":1143,"language":342,"meta":153,"style":153},"@spec interval_from_opts(interval_opts()) :: interval()\n",[117,1145,1146],{"__ignoreMap":153},[110,1147,1148],{"class":347,"line":348},[110,1149,1143],{},[107,1151,1152,1153,1155,1156,1158,1159,1161,1162,309],{},"Build the interval map from an option list. A missing side stays\n",[117,1154,159],{}," (unbounded). The default closings are ",[117,1157,700],{}," on the left\nand ",[117,1160,696],{}," on the right, the half-open convention that lets\nadjacent intervals tile without overlap; an absent side gets ",[117,1163,159],{},[145,1165,1168],{"className":1166,"code":1167,"language":150},[148],"iex> Zocam.Intervals.interval_from_opts(from: ~T[09:00:00])\n%{from: ~T[09:00:00], until: nil, left: :closed, right: nil}\n",[117,1169,1167],{"__ignoreMap":153},[333,1171,1173],{"id":1172},"is_interval",[117,1174,1172],{},[107,1176,1177],{},[457,1178,1179],{},"macro",[333,1181,1183],{"id":1182},"new",[117,1184,1185],{},"new!",[145,1187,1189],{"className":340,"code":1188,"language":342,"meta":153,"style":153},"@spec new!(interval_opts()) :: interval()\n",[117,1190,1191],{"__ignoreMap":153},[110,1192,1193],{"class":347,"line":348},[110,1194,1188],{},[107,1196,1197],{},"Build ONE interval — a piece, not a set. This is the piece\nconstructor: an operation on sets answers with a set, and a piece\nconstructor answers with a piece (see \"One rule for shapes\" in the\nmoduledoc).",[107,1199,1200,1201,1203,1204,1206],{},"The common call, with the default closings (",[117,1202,700],{}," left,\n",[117,1205,696],{}," right):",[145,1208,1211],{"className":1209,"code":1210,"language":150},[148],"iex> Zocam.Intervals.new!(from: ~T[09:00:00], until: ~T[17:00:00])\n%{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n",[117,1212,1210],{"__ignoreMap":153},[107,1214,1215,1216,1218],{},"The edge: a missing side stays ",[117,1217,159],{}," — the piece is a ray:",[145,1220,1223],{"className":1221,"code":1222,"language":150},[148],"iex> Zocam.Intervals.new!(from: ~T[09:00:00])\n%{from: ~T[09:00:00], until: nil, left: :closed, right: nil}\n",[117,1224,1222],{"__ignoreMap":153},[107,1226,292,1227,1229,1230,839],{},[117,1228,208],{}," does not build a set. To turn pieces into a\nset, pour them through a set operation, for example ",[117,1231,175],{},[145,1233,1236],{"className":1234,"code":1235,"language":150},[148],"iex> Zocam.Intervals.compress([\n...>   Zocam.Intervals.new!(from: ~T[09:00:00], until: ~T[12:00:00]),\n...>   Zocam.Intervals.new!(from: ~T[12:00:00], until: ~T[17:00:00])\n...> ])\n%Zocam.Intervals{intervals: [%{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}]}\n",[117,1237,1235],{"__ignoreMap":153},[107,1239,1240,1241,1243],{},"Raises ",[117,1242,233],{}," on an empty specification:",[145,1245,1248],{"className":1246,"code":1247,"language":150},[148],"iex> Zocam.Intervals.new!([])\n** (ArgumentError) Invalid options: from or until must be specified\n",[117,1249,1247],{"__ignoreMap":153},[333,1251,1253],{"id":1252},"overlaps",[117,1254,1255],{},"overlaps?",[145,1257,1259],{"className":340,"code":1258,"language":342,"meta":153,"style":153},"@spec overlaps?(at_least_one_valid(), at_least_one_valid()) :: boolean()\n",[117,1260,1261],{"__ignoreMap":153},[110,1262,1263],{"class":347,"line":348},[110,1264,1258],{},[107,1266,1267],{},"Do the two operands share at least one instant? Both operands may\nbe pieces or whole sets (any funnel shape); two sets overlap when\nany pair of their members does. This is a question, so the answer\nis a boolean, not a set.",[107,1269,1270,1271,1274,1275,1278,1279,1281],{},"The pitfall: touching is not overlapping. ",[117,1272,1273],{},"[09:00, 12:00)"," and\n",[117,1276,1277],{},"[12:00, 17:00)"," tile without a shared instant, because the right\nside is ",[117,1280,696],{},". Close it and the instant 12:00 is shared:",[145,1283,1286],{"className":1284,"code":1285,"language":150},[148],"iex> Zocam.Intervals.overlaps?(\n...>   %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :open},\n...>   %{from: ~T[12:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n...> )\nfalse\n\niex> Zocam.Intervals.overlaps?(\n...>   %{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :closed},\n...>   %{from: ~T[12:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n...> )\ntrue\n",[117,1287,1285],{"__ignoreMap":153},[333,1289,1291],{"id":1290},"union",[117,1292,1290],{},[145,1294,1296],{"className":340,"code":1295,"language":342,"meta":153,"style":153},"@spec union(at_least_one_valid(), at_least_one_valid()) :: t()\n",[117,1297,1298],{"__ignoreMap":153},[110,1299,1300],{"class":347,"line":348},[110,1301,1295],{},[107,1303,1304,1305,1307],{},"The union of the two operands, as one normal-form set (see\n",[117,1306,175],{},"): overlapping and touching pieces fuse into one\nspanning interval.",[145,1309,1312],{"className":1310,"code":1311,"language":150},[148],"iex> Zocam.Intervals.union(\n...>   %{from: ~T[09:00:00], until: ~T[11:00:00], left: :closed, right: :open},\n...>   %{from: ~T[10:00:00], until: ~T[13:00:00], left: :closed, right: :open}\n...> )\n%Zocam.Intervals{intervals: [%{from: ~T[09:00:00], until: ~T[13:00:00], left: :closed, right: :open}]}\n",[117,1313,1311],{"__ignoreMap":153},[107,1315,1316],{},"Touching pieces fuse too, because the shared instant is covered:",[145,1318,1321],{"className":1319,"code":1320,"language":150},[148],"iex> Zocam.Intervals.union(\n...>   [%{from: ~T[09:00:00], until: ~T[12:00:00], left: :closed, right: :open}],\n...>   %{from: ~T[12:00:00], until: ~T[17:00:00], left: :closed, right: :open}\n...> )\n%Zocam.Intervals{intervals: [%{from: ~T[09:00:00], until: ~T[17:00:00], left: :closed, right: :open}]}\n",[117,1322,1320],{"__ignoreMap":153},[107,1324,1325,1326,1328],{},"The edge: when both touching sides are ",[117,1327,696],{},", the shared instant\nis in neither interval. A hole of one instant remains, so the two\ndo not fuse:",[145,1330,1333],{"className":1331,"code":1332,"language":150},[148],"iex> Zocam.Intervals.union(\n...>   [%{from: ~T[09:00:00], until: ~T[12:00:00], left: :open, right: :open}],\n...>   %{from: ~T[12:00:00], until: ~T[17:00:00], left: :open, right: :open}\n...> ).intervals |> length()\n2\n",[117,1334,1332],{"__ignoreMap":153},[1336,1337],"hr",{},[107,1339,1340],{},[457,1341,1342,1343,1347],{},"Consult ",[122,1344,1346],{"href":1345},"api-reference","api-reference.md"," for complete listing",[1349,1350,1351],"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":153,"searchDepth":368,"depth":368,"links":1353},[1354,1355,1356,1369],{"id":183,"depth":368,"text":184},{"id":264,"depth":368,"text":265},{"id":330,"depth":368,"text":331,"children":1357},[1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368],{"id":335,"depth":374,"text":335},{"id":353,"depth":374,"text":353},{"id":398,"depth":374,"text":398},{"id":412,"depth":374,"text":412},{"id":465,"depth":374,"text":465},{"id":479,"depth":374,"text":479},{"id":493,"depth":374,"text":493},{"id":507,"depth":374,"text":507},{"id":543,"depth":374,"text":543},{"id":557,"depth":374,"text":557},{"id":571,"depth":374,"text":571},{"id":585,"depth":368,"text":586,"children":1370},[1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385],{"id":589,"depth":374,"text":592},{"id":678,"depth":374,"text":681},{"id":735,"depth":374,"text":738},{"id":790,"depth":374,"text":793},{"id":848,"depth":374,"text":851},{"id":889,"depth":374,"text":889},{"id":937,"depth":374,"text":940},{"id":972,"depth":374,"text":972},{"id":1029,"depth":374,"text":1029},{"id":1094,"depth":374,"text":1094},{"id":1138,"depth":374,"text":1138},{"id":1172,"depth":374,"text":1172},{"id":1182,"depth":374,"text":1185},{"id":1252,"depth":374,"text":1255},{"id":1290,"depth":374,"text":1290},"The linear kernel: sets of concrete intervals on one axis, with union, intersection, complement, and difference.","md",null,{},true,{"title":77,"description":1386},"6kSDTg6aOq666LVeOVo9B8XdtJHw1AysOp6syhOaDtM",[1394,1396],{"title":73,"path":74,"stem":75,"description":1395,"children":-1},"A three-layer library for time: calendar things, sets of them, and concrete intervals.",{"title":81,"path":82,"stem":83,"description":1397,"children":-1},"One calendar, checked at the door: Calendar.ISO.",1786065168174]