<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="391188" data-post-id="391188">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="billylanchantin" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/billylanchantin/120/28428_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  billylanchantin
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>So glad to see the additional release of <code>ex_tempo_sql</code>! (BTW the link isn’t working for me?)</p>
<p>I’ve been following Tempo off and on and I’d had the thought that a <code>tstz{multi}range</code> in Postgres would be a good way to persist that data. I’m happy to see that my instinct wasn’t too off the mark. And now that there’s a persistence story, it makes me want to start playing with Tempo for more serious uses.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="391188" data-batch-url="/posts/batch_likers">
                        1
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/tempo-a-unified-time-type-that-models-time-as-interval-sets-not-instants/75083/42">Post #41</a>
	                </div>
	            </div>
              <div id="likers-container-391188" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="391188"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #41"></div>
  </section>
</div>
    <div class="postbit" id="391200" data-post-id="391200">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="kip" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/kip/120/1440_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  kip
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>ex_cldr Core Team</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="billylanchantin" data-post="42" data-topic="75083">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/billylanchantin/48/28428_2.png" class="avatar"> billylanchantin:</div>
<blockquote>
<p>BTW the link isn’t working for me?</p>
</blockquote>
</aside>
<p>Thanks for letting me know, link fixed now.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="391200" data-batch-url="/posts/batch_likers">
                        1
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/tempo-a-unified-time-type-that-models-time-as-interval-sets-not-instants/75083/43">Post #42</a>
	                </div>
	            </div>
              <div id="likers-container-391200" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="391200"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #42"></div>
  </section>
</div>
    <div class="postbit" id="391277" data-post-id="391277">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="kip" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/kip/120/1440_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  kip
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>ex_cldr Core Team</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Sometimes  interesting (at least to me!) use cases surface. Since <code>Tempo</code> is a different way of thinking about time, it can take a while for cognition to switch. Therefore I think posting interesting recipes from time-to-time (no pun intended) might be useful.</p>
<h3><a name="p-391277-calculating-business-days-in-a-year-1" class="anchor" href="#p-391277-calculating-business-days-in-a-year-1" aria-label="Heading link" rel="nofollow"></a>Calculating business days in a year</h3>
<p><a class="mention" href="/u/tubedude" rel="nofollow">@tubedude</a> is working on a <a href="https://hex.pm/packages/finance" rel="nofollow">Financial Calulation</a> library. Turns out that there are <a href="https://www.investopedia.com/terms/d/daycount.asp#:~:text=actual%2F360%3A%20calculates%20the%20daily,days%20in%20each%20time%20period." rel="nofollow">many ways to count days</a> when calculating daily interest. Most of them are straight forward.</p>
<p>However, Brazil has a <a href="https://www.anbima.com.br/feriados/feriados.asp" rel="nofollow">particular</a> way of determining the day count: All of the working days of the year, not including the holidays declared by ANBIMA.</p>
<h3><a name="p-391277-business252-brazil-fixed-income-instruments-2" class="anchor" href="#p-391277-business252-brazil-fixed-income-instruments-2" aria-label="Heading link" rel="nofollow"></a>Business/252: Brazil fixed-income instruments</h3>
<p>Here’s the recipe, following which we’ll work through the code step-by-step.</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">import Tempo.Sigils
{:ok, holidays} = Tempo.ICal.from_ical_file("feriados_anbima.ics")

settlement = ~o"2024-01-01"
maturity   = ~o"2025-01-01"

{:ok, window} = Tempo.Interval.new(from: settlement, to: maturity)
{:ok, workdays} = Tempo.select(window, Tempo.workdays(:BR))
{:ok, business_days} = Tempo.members_outside(workdays, holidays)

Tempo.IntervalSet.count(business_days) / 252
#=&gt; 1.003968253968254   (253 business days in 2024)
</code></pre>
<h3><a name="p-391277-walkthrough-of-the-recipe-3" class="anchor" href="#p-391277-walkthrough-of-the-recipe-3" aria-label="Heading link" rel="nofollow"></a>Walkthrough of the recipe</h3>
<p>The recipe leverages the fact that in Tempo, everything is an <em>interval</em> (not an <em>instant</em>). And therefore, operations on an interval are <em>set</em> operations, not scalar.</p>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Import the ~o sigil which compiles an ISO8601 time expression
# into a `Tempo.t` struct at compile time. 
iex&gt; import Tempo.Sigils

# Import an `.ics` (iCalendar) file and interpret each event as a 
# Tempo interval. This uses the fabulous [ical](https://hex.pm/packages/ical) library for parsing
# the calendar file. The data itself comes from [ANBIMA](https://www.anbima.com.br/feriados/feriados.asp) and is converted to an iCal file by [a script](https://github.com/kipcole9/tempo/blob/main/scripts/anbima_xls_to_ics.py).
iex&gt; {:ok, holidays} = Tempo.ICal.from_ical_file("feriados_anbima.ics")
{:ok, #Tempo.IntervalSet&lt;[#Tempo.Interval&lt;~o"2001Y1M1D/2001Y1M2D" · Confraternização Universal&gt;, #Tempo.Interval&lt;~o"2001Y2M26D/2001Y2M27D" · Carnaval&gt;, #Tempo.Interval&lt;~o"2001Y2M27D/2001Y2M28D" · Carnaval&gt;, #Tempo.Interval&lt;~o"2001Y4M13D/2001Y4M14D", ....]&gt;

# Now we describe the settlement and maturity dates. These look
# a lot like a ~D[2024-01-01] date in Elixir. But unlike the native 
# Date type, ~o[2024-01-01] is describing an *implicit* interval of
# one year. We could also have entered it as an *explicit* interval
# as ~o"2024-01-01/2024-01-02". Note that Tempo intervals are
# [half-open](https://en.wikipedia.org/wiki/Interval_(mathematics))
iex&gt; settlement = ~o"2024-01-01"
iex&gt; maturity   = ~o"2025-01-01"

# We are explicitly creating the interval for a loan here, we could 
# also have used a the direct notation ~o"2024-01-01/2024-01-02"
# we described previously.
iex&gt; {:ok, window} = Tempo.Interval.new(from: settlement, to: maturity)

# From all the days in the year, we want to select only
# those days that are working days. Since Tempo leans on 
# [Localize](https://hex.pm/packages/localize), we can derive
# the working days directly for any given [ISO 3166](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)
# territory code. Notice that the return value is also a Tempo
# interval. In this case its the interval that selects the days of the
# week 1 through 5 denoting Monday through Friday. Different
# territories define different working weeks (try the example
# with the territory :SA).
iex&gt; brazil_workdays = Tempo.workdays(:BR)
~o"{1,2,3,4,5}K"

# Now we're getting the heart of Tempo's power. Given the
# full year interval we described in `window`, select only the
# working days. This is more formally a *projection*, but `select`
# is a more common idiom so we use that.
# Tempo.select/2` picks the sub-intervals of a span that match a
# selector (workdays, the 15th, every Friday, holidays…) and
# returns them as an `IntervalSet`.
iex&gt; {:ok, workdays} = Tempo.select(window, brazil_workdays)
{:ok,
 #Tempo.IntervalSet&lt;[~o"2024Y1M1D/2024Y1M2D", ~o"2024Y1M2D/2024Y1M3D", ~o"2024Y1M3D/2024Y1M4D", ~o"2024Y1M4D/2024Y1M5D", ~o"2024Y1M5D/2024Y1M6D", ...]&gt;

# Tempo.members_outside is a member-preserving anti-overlap 
# filter — returns the whole members of workdays that do NOT 
# overlap any member of holidays.
iex&gt; {:ok, business_days} = Tempo.members_outside(workdays, holidays)
{:ok,
 #Tempo.IntervalSet&lt;[~o"2024Y1M2D/2024Y1M3D", ~o"2024Y1M3D/2024Y1M4D", ~o"2024Y1M4D/2024Y1M5D", ~o"2024Y1M5D/2024Y1M6D", ~o"2024Y1M8D/2024Y1M9D", ~o"2024Y1M9D/2024Y1M10D", ~o"2024Y1M10D/2024Y1M11D", ~o"2024Y1M11D/2024Y1M12D", ...]&gt;

# And how any days is that? It's the number of spans in the
# IntervalSet, so we can just count them.
iex&gt; Tempo.IntervalSet.count(business_days)
253
</code></pre>
<h3><a name="p-391277-trying-this-at-home-4" class="anchor" href="#p-391277-trying-this-at-home-4" aria-label="Heading link" rel="nofollow"></a>Trying this at home</h3>
<p>All the pieces are in the documentation and the GitHub repo.</p>
<ul>
<li>Script to convert the ANBIMA holiday spreadsheet into an <code>.ics</code> file: <a href="https://github.com/kipcole9/tempo/blob/main/scripts/anbima_xls_to_ics.py" class="inline-onebox" rel="nofollow">tempo/scripts/anbima_xls_to_ics.py at main · kipcole9/tempo · GitHub</a></li>
<li>Documentation of the recipe in the cookbook: <a href="https://ex-tempo.hexdocs.pm/cookbook.html#business-252-brazil-s-business-day-year-fraction" class="inline-onebox" rel="nofollow">Tempo Cookbook — Tempo v0.18.1</a></li>
</ul> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="391277" data-batch-url="/posts/batch_likers">
                        7
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/tempo-a-unified-time-type-that-models-time-as-interval-sets-not-instants/75083/44">Post #43</a>
	                </div>
	            </div>
              <div id="likers-container-391277" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="391277"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #43"></div>
  </section>
</div>
    <div class="postbit" id="391316" data-post-id="391316">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="kip" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/kip/120/1440_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  kip
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>ex_cldr Core Team</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Here’s today’s recipe from the <em>interval-sets-not-instants</em> cookbook. This one is a bit of a Tempo party trick: a set operation across two different calendars.</p>
<h3><a name="p-391316-when-payroll-and-the-law-are-in-different-calendars-1" class="anchor" href="#p-391316-when-payroll-and-the-law-are-in-different-calendars-1" aria-label="Heading link" rel="nofollow"></a>When payroll and the law are in different calendars</h3>
<p>UAE labour law (<a href="https://u.ae/en/information-and-services/jobs/employment-in-the-private-sector/working-hours" rel="nofollow">Article 17 of Federal Decree-Law No. 33 of 2021</a>) reduces the private-sector workday by two hours - eight becomes six - for every day of Ramadan, for all employees regardless of religion, with no pay reduction.</p>
<p>The payroll year is Gregorian. Ramadan is the ninth month of the Islamic calendar, and because the Islamic year is ~11 days shorter, Ramadan drifts earlier through the Gregorian year - February one year, January a few years later. So “how many statutory working hours in 2026?” is an intersection between a Gregorian year and an Islamic month. Most (all?) date libraries can’t represent that question. In Tempo it’s the same set algebra as everything else.</p>
<h3><a name="p-391316-recipe-for-statutory-hours-for-2026-2" class="anchor" href="#p-391316-recipe-for-statutory-hours-for-2026-2" aria-label="Heading link" rel="nofollow"></a>Recipe for statutory hours for 2026</h3>
<pre data-code-wrap="elixir"><code class="lang-elixir">import Tempo.Sigils

work_year = ~o"2026"
ramadan = ~o"1447-09[u-ca=islamic-civil]"

standard_hours = 8
ramadan_hours  = 6

{:ok, workdays} = Tempo.select(work_year, Tempo.workdays(:AE))
{:ok, ramadan_workdays} = Tempo.members_overlapping(workdays, ramadan)
{:ok, normal_workdays} = Tempo.members_outside(workdays, ramadan)

Tempo.IntervalSet.count(normal_workdays) * standard_hours +
  Tempo.IntervalSet.count(ramadan_workdays) * ramadan_hours
#=&gt; 2044   (239 normal workdays × 8h + 22 Ramadan workdays × 6h — 44 hours reduced)
</code></pre>
<h3><a name="p-391316-walkthrough-of-the-recipe-3" class="anchor" href="#p-391316-walkthrough-of-the-recipe-3" aria-label="Heading link" rel="nofollow"></a>Walkthrough of the recipe</h3>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Ramadan is month 9 of the Islamic year — here 1447 AH, written with
# the IXDTF u-ca suffix. The value lives in the Islamic calendar; its
# implicit span is the whole month, in its own calendar's terms:
iex&gt; ramadan = ~o"1447-09[u-ca=islamic-civil]"
iex&gt; Tempo.to_interval(ramadan)
{:ok, ~o"1447Y9M1D[u-ca=islamic-civil]/1447Y10M1D[u-ca=islamic-civil]"}

# Where does that land in the Gregorian year? Intersect it with 2026.
# Tempo converts calendars inside the set operation — note the result
# is plain Gregorian:
iex&gt; Tempo.intersection(~o"2026", ramadan)
{:ok, #Tempo.IntervalSet&lt;[~o"2026Y2M18D/2026Y3M20D"]&gt;}

# The UAE's workweek comes from CLDR territory data — and it knows
# the UAE moved its weekend to Saturday–Sunday in 2022, so workdays
# are Monday–Friday. No hand-coded weekday list:
iex&gt; Tempo.workdays(:AE)
~o"{1,2,3,4,5}K"

iex&gt; {:ok, workdays} = Tempo.select(~o"2026", Tempo.workdays(:AE))
iex&gt; Tempo.IntervalSet.count(workdays)
261

# Now partition the year's workdays by Ramadan. `members_overlapping`
# keeps the workdays that fall inside it; `members_outside` keeps the
# rest. Same member-preserving filters as the Business/252 recipe -
# one Gregorian operand, one Islamic:
iex&gt; {:ok, ramadan_workdays} = Tempo.members_overlapping(workdays, ramadan)
iex&gt; ramadan_workdays
#Tempo.IntervalSet&lt;[~o"2026Y2M18D/2026Y2M19D", ~o"2026Y2M19D/2026Y2M20D", ~o"2026Y2M20D/2026Y2M21D", …]&gt;

iex&gt; Tempo.IntervalSet.count(ramadan_workdays)
22

# 22 six-hour days and 239 (being 261 - 22) eight-hour days:
iex&gt; 239 * 8 + 22 * 6
2044
</code></pre>
<p>An additional benefit of doing this symbolically is that next year is the same pipeline with two new bindings. Ramadan 1448 drifts ten days earlier, and nothing else changes:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">iex&gt; Tempo.intersection(~o"2027", ~o"1448-09[u-ca=islamic-civil]")
{:ok, #Tempo.IntervalSet&lt;[~o"2027Y2M8D/2027Y3M10D"]&gt;}
</code></pre>
<p>Two real world caveats:. The tabular <code>islamic-civil</code> calendar is a planning approximation - the legal month begins with the moon-sighting announcement. And Eid al-Fitr, immediately after Ramadan, is a public holiday - subtract it with <code>Tempo.members_outside/2</code> exactly as Business/252 subtracts ANBIMA holidays.</p>
<h3><a name="p-391316-trying-this-at-home-4" class="anchor" href="#p-391316-trying-this-at-home-4" aria-label="Heading link" rel="nofollow"></a>Trying this at home</h3>
<ul>
<li>The recipe in the cookbook: <a href="https://ex-tempo.hexdocs.pm/cookbook.html#ramadan-working-hours-statutory-hours-across-two-calendars" class="inline-onebox" rel="nofollow">Tempo Cookbook — Tempo v0.19.1</a></li>
<li>The supported calendars (Islamic civil/tabular/Umm al-Qura, Hebrew, Coptic, Ethiopic, Persian, …) come from <a href="https://hex.pm/packages/calendrical" rel="nofollow">Calendrical</a>; the <code>u-ca</code> tag vocabulary is CLDR’s, via <a href="https://hex.pm/packages/localize" rel="nofollow">Localize</a>.</li>
<li>Cross-calendar comparison semantics: <a href="https://ex-tempo.hexdocs.pm/cookbook.html#9-cross-calendar-and-cross-timezone" class="inline-onebox" rel="nofollow">Tempo Cookbook — Tempo v0.19.1</a></li>
</ul> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="391316" data-batch-url="/posts/batch_likers">
                        5
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/tempo-a-unified-time-type-that-models-time-as-interval-sets-not-instants/75083/45">Post #44</a>
	                </div>
	            </div>
              <div id="likers-container-391316" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="391316"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-last-post cat-last-post" title="Last post!"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <span class="all-loaded">— All posts loaded —</span>
</div></template></turbo-stream>