Calendar Cycle Calculation Logic closes #276 #284
2 changed files with 594 additions and 0 deletions
267
lib/mv/membership_fees/calendar_cycles.ex
Normal file
267
lib/mv/membership_fees/calendar_cycles.ex
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
defmodule Mv.MembershipFees.CalendarCycles do
|
||||
@moduledoc """
|
||||
Calendar-based cycle calculation functions for membership fees.
|
||||
|
||||
This module provides pure functions for calculating cycle boundaries
|
||||
based on interval types (monthly, quarterly, half-yearly, yearly).
|
||||
|
||||
All functions are pure (no side effects) and work with Elixir's `Date` type.
|
||||
|
||||
## Interval Types
|
||||
|
||||
- `:monthly` - Cycles from 1st to last day of each month
|
||||
- `:quarterly` - Cycles from 1st of Jan/Apr/Jul/Oct to last day of quarter
|
||||
- `:half_yearly` - Cycles from 1st of Jan/Jul to last day of half-year
|
||||
- `:yearly` - Cycles from Jan 1st to Dec 31st
|
||||
|
||||
## Examples
|
||||
|
||||
iex> date = ~D[2024-03-15]
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(date, :monthly)
|
||||
~D[2024-03-01]
|
||||
|
||||
iex> cycle_start = ~D[2024-01-01]
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle_start, :yearly)
|
||||
~D[2024-12-31]
|
||||
|
||||
iex> cycle_start = ~D[2024-01-01]
|
||||
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(cycle_start, :yearly)
|
||||
~D[2025-01-01]
|
||||
"""
|
||||
|
||||
@type interval :: :monthly | :quarterly | :half_yearly | :yearly
|
||||
|
||||
@doc """
|
||||
Calculates the start date of the cycle that contains the given date.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `date` - The date for which to find the cycle start
|
||||
- `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
|
||||
- `reference_date` - Optional reference date (defaults to `date`)
|
||||
|
||||
## Returns
|
||||
|
||||
The start date of the cycle containing the given date.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-03-15], :monthly)
|
||||
~D[2024-03-01]
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-05-15], :quarterly)
|
||||
~D[2024-04-01]
|
||||
|
moritz marked this conversation as resolved
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-09-15], :half_yearly)
|
||||
~D[2024-07-01]
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-12-15], :yearly)
|
||||
~D[2024-01-01]
|
||||
"""
|
||||
@spec calculate_cycle_start(Date.t(), interval(), Date.t() | nil) :: Date.t()
|
||||
def calculate_cycle_start(date, interval, reference_date \\ nil) do
|
||||
reference = reference_date || date
|
||||
|
||||
case interval do
|
||||
:monthly -> monthly_cycle_start(reference)
|
||||
:quarterly -> quarterly_cycle_start(reference)
|
||||
:half_yearly -> half_yearly_cycle_start(reference)
|
||||
:yearly -> yearly_cycle_start(reference)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the end date of a cycle based on its start date and interval.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the cycle
|
||||
- `interval` - The interval type
|
||||
|
||||
## Returns
|
||||
|
||||
The end date of the cycle.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-03-01], :monthly)
|
||||
~D[2024-03-31]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-02-01], :monthly)
|
||||
~D[2024-02-29]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :quarterly)
|
||||
~D[2024-03-31]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :half_yearly)
|
||||
~D[2024-06-30]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :yearly)
|
||||
~D[2024-12-31]
|
||||
"""
|
||||
@spec calculate_cycle_end(Date.t(), interval()) :: Date.t()
|
||||
def calculate_cycle_end(cycle_start, interval) do
|
||||
case interval do
|
||||
:monthly -> monthly_cycle_end(cycle_start)
|
||||
:quarterly -> quarterly_cycle_end(cycle_start)
|
||||
:half_yearly -> half_yearly_cycle_end(cycle_start)
|
||||
:yearly -> yearly_cycle_end(cycle_start)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the start date of the next cycle.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the current cycle
|
||||
- `interval` - The interval type
|
||||
|
||||
## Returns
|
||||
|
||||
The start date of the next cycle.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :monthly)
|
||||
~D[2024-02-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :quarterly)
|
||||
~D[2024-04-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :half_yearly)
|
||||
~D[2024-07-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :yearly)
|
||||
~D[2025-01-01]
|
||||
"""
|
||||
@spec next_cycle_start(Date.t(), interval()) :: Date.t()
|
||||
def next_cycle_start(cycle_start, interval) do
|
||||
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||
next_date = Date.add(cycle_end, 1)
|
||||
calculate_cycle_start(next_date, interval)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the cycle contains today's date.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the cycle
|
||||
- `interval` - The interval type
|
||||
|
||||
## Returns
|
||||
|
||||
`true` if today is within the cycle, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming today is 2024-03-15
|
||||
iex> current_cycle?(~D[2024-03-01], :monthly)
|
||||
true
|
||||
|
||||
iex> current_cycle?(~D[2024-02-01], :monthly)
|
||||
false
|
||||
"""
|
||||
@spec current_cycle?(Date.t(), interval()) :: boolean()
|
||||
def current_cycle?(cycle_start, interval) do
|
||||
today = Date.utc_today()
|
||||
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
Date.compare(cycle_start, today) in [:lt, :eq] and
|
||||
Date.compare(today, cycle_end) in [:lt, :eq]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the cycle was just completed (ended yesterday or earlier, but is the most recent completed cycle).
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the cycle
|
||||
- `interval` - The interval type
|
||||
|
||||
## Returns
|
||||
|
||||
`true` if the cycle ended yesterday or earlier and is the last completed cycle, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming today is 2024-04-01 (cycle ended yesterday)
|
||||
iex> last_completed_cycle?(~D[2024-03-01], :monthly)
|
||||
true
|
||||
|
||||
# Assuming today is 2024-03-15 (cycle is still current)
|
||||
iex> last_completed_cycle?(~D[2024-03-01], :monthly)
|
||||
false
|
||||
"""
|
||||
@spec last_completed_cycle?(Date.t(), interval()) :: boolean()
|
||||
def last_completed_cycle?(cycle_start, interval) do
|
||||
today = Date.utc_today()
|
||||
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
# Cycle must have ended (yesterday or earlier)
|
||||
case Date.compare(today, cycle_end) do
|
||||
:gt ->
|
||||
# Check if this is the most recent completed cycle
|
||||
# by verifying that the next cycle hasn't ended yet
|
||||
next_start = next_cycle_start(cycle_start, interval)
|
||||
next_end = calculate_cycle_end(next_start, interval)
|
||||
|
||||
Date.compare(today, next_end) in [:lt, :eq]
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Private helper functions
|
||||
|
||||
defp monthly_cycle_start(date) do
|
||||
Date.new!(date.year, date.month, 1)
|
||||
end
|
||||
|
||||
defp monthly_cycle_end(cycle_start) do
|
||||
Date.end_of_month(cycle_start)
|
||||
end
|
||||
|
||||
defp quarterly_cycle_start(date) do
|
||||
quarter_start_month =
|
||||
case date.month do
|
||||
m when m in [1, 2, 3] -> 1
|
||||
m when m in [4, 5, 6] -> 4
|
||||
m when m in [7, 8, 9] -> 7
|
||||
m when m in [10, 11, 12] -> 10
|
||||
end
|
||||
|
||||
Date.new!(date.year, quarter_start_month, 1)
|
||||
end
|
||||
|
||||
defp quarterly_cycle_end(cycle_start) do
|
||||
case cycle_start.month do
|
||||
1 -> Date.new!(cycle_start.year, 3, 31)
|
||||
4 -> Date.new!(cycle_start.year, 6, 30)
|
||||
7 -> Date.new!(cycle_start.year, 9, 30)
|
||||
10 -> Date.new!(cycle_start.year, 12, 31)
|
||||
end
|
||||
end
|
||||
|
||||
defp half_yearly_cycle_start(date) do
|
||||
half_start_month = if date.month in 1..6, do: 1, else: 7
|
||||
Date.new!(date.year, half_start_month, 1)
|
||||
end
|
||||
|
||||
defp half_yearly_cycle_end(cycle_start) do
|
||||
case cycle_start.month do
|
||||
1 -> Date.new!(cycle_start.year, 6, 30)
|
||||
7 -> Date.new!(cycle_start.year, 12, 31)
|
||||
end
|
||||
end
|
||||
|
||||
defp yearly_cycle_start(date) do
|
||||
Date.new!(date.year, 1, 1)
|
||||
end
|
||||
|
||||
defp yearly_cycle_end(cycle_start) do
|
||||
Date.new!(cycle_start.year, 12, 31)
|
||||
end
|
||||
end
|
||||
327
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
327
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
defmodule Mv.MembershipFees.CalendarCyclesTest do
|
||||
@moduledoc """
|
||||
Tests for CalendarCycles module.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
describe "calculate_cycle_start/3" do
|
||||
test "monthly: returns 1st of month for any date" do
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-31], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-01], :monthly) == ~D[2024-03-01]
|
||||
end
|
||||
|
||||
test "quarterly: returns 1st of quarter (Jan/Apr/Jul/Oct)" do
|
||||
# Q1 (Jan-Mar)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-02-15], :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :quarterly) == ~D[2024-01-01]
|
||||
|
||||
# Q2 (Apr-Jun)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-04-15], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :quarterly) == ~D[2024-04-01]
|
||||
|
||||
# Q3 (Jul-Sep)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-07-15], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-08-15], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :quarterly) == ~D[2024-07-01]
|
||||
|
||||
# Q4 (Oct-Dec)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-10-15], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-11-15], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :quarterly) == ~D[2024-10-01]
|
||||
end
|
||||
|
||||
test "half_yearly: returns 1st of half (Jan/Jul)" do
|
||||
# First half (Jan-Jun)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :half_yearly) == ~D[2024-01-01]
|
||||
|
||||
# Second half (Jul-Dec)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-07-15], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :half_yearly) == ~D[2024-07-01]
|
||||
end
|
||||
|
||||
test "yearly: returns 1st of January" do
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :yearly) == ~D[2024-01-01]
|
||||
end
|
||||
|
||||
test "uses reference_date when provided" do
|
||||
date = ~D[2024-03-15]
|
||||
reference = ~D[2024-05-20]
|
||||
|
||||
assert CalendarCycles.calculate_cycle_start(date, :monthly, reference) == ~D[2024-05-01]
|
||||
assert CalendarCycles.calculate_cycle_start(date, :quarterly, reference) == ~D[2024-04-01]
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculate_cycle_end/2" do
|
||||
test "monthly: returns last day of month" do
|
||||
# 31-day month
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly) == ~D[2024-03-31]
|
||||
|
||||
# 30-day month
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :monthly) == ~D[2024-04-30]
|
||||
|
||||
# February in leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly) == ~D[2024-02-29]
|
||||
|
||||
# February in non-leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2023-02-01], :monthly) == ~D[2023-02-28]
|
||||
end
|
||||
|
||||
test "quarterly: returns last day of quarter" do
|
||||
# Q1: Jan-Mar -> Mar 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly) == ~D[2024-03-31]
|
||||
|
||||
# Q2: Apr-Jun -> Jun 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :quarterly) == ~D[2024-06-30]
|
||||
|
||||
# Q3: Jul-Sep -> Sep 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :quarterly) == ~D[2024-09-30]
|
||||
|
||||
# Q4: Oct-Dec -> Dec 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-10-01], :quarterly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "half_yearly: returns last day of half-year" do
|
||||
# First half: Jan-Jun -> Jun 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly) == ~D[2024-06-30]
|
||||
|
||||
# Second half: Jul-Dec -> Dec 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :half_yearly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "yearly: returns Dec 31" do
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly) == ~D[2024-12-31]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2023-01-01], :yearly) == ~D[2023-12-31]
|
||||
end
|
||||
end
|
||||
|
||||
describe "next_cycle_start/2" do
|
||||
test "monthly: adds one month" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :monthly) == ~D[2024-02-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-02-01], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-12-01], :monthly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "quarterly: adds three months" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-04-01], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-10-01], :quarterly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "half_yearly: adds six months" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :half_yearly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "yearly: adds one year" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2023-01-01], :yearly) == ~D[2024-01-01]
|
||||
end
|
||||
end
|
||||
|
||||
describe "current_cycle?/2" do
|
||||
test "returns true when today is within cycle" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
|
||||
test "returns true when today equals cycle start" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = today
|
||||
|
||||
# For monthly, if today is the 1st, it's the cycle start
|
||||
if today.day == 1 do
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns true when today equals cycle end" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, :monthly)
|
||||
|
||||
# If today is the last day of the month, it's the cycle end
|
||||
if today == cycle_end do
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when today is before cycle start" do
|
||||
future_date = Date.add(Date.utc_today(), 35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(future_date, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "returns false when today is after cycle end" do
|
||||
past_date = Date.add(Date.utc_today(), -35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(past_date, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "works for all interval types" do
|
||||
today = Date.utc_today()
|
||||
|
||||
for interval <- [:monthly, :quarterly, :half_yearly, :yearly] do
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, interval)
|
||||
result = CalendarCycles.current_cycle?(cycle_start, interval)
|
||||
|
||||
assert result == true, "Expected current cycle for #{interval} with start #{cycle_start}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "last_completed_cycle?/2" do
|
||||
test "returns true when cycle ended yesterday" do
|
||||
yesterday = Date.add(Date.utc_today(), -1)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(yesterday, :monthly)
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, :monthly)
|
||||
|
||||
# Only test if yesterday was actually the cycle end
|
||||
if yesterday == cycle_end do
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when cycle is still current" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "returns false when cycle is in the future" do
|
||||
future_date = Date.add(Date.utc_today(), 35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(future_date, :monthly)
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "returns false when next cycle has also ended" do
|
||||
# Use a date from two cycles ago
|
||||
past_date = Date.add(Date.utc_today(), -65)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(past_date, :monthly)
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "works correctly for quarterly intervals" do
|
||||
# Test with a known past quarter
|
||||
past_quarter_start = ~D[2024-01-01]
|
||||
today = Date.utc_today()
|
||||
|
||||
if Date.compare(today, CalendarCycles.calculate_cycle_end(past_quarter_start, :quarterly)) ==
|
||||
:gt do
|
||||
# Check if next quarter hasn't ended yet
|
||||
next_quarter_start = CalendarCycles.next_cycle_start(past_quarter_start, :quarterly)
|
||||
next_quarter_end = CalendarCycles.calculate_cycle_end(next_quarter_start, :quarterly)
|
||||
|
||||
if Date.compare(today, next_quarter_end) in [:lt, :eq] do
|
||||
assert CalendarCycles.last_completed_cycle?(past_quarter_start, :quarterly) == true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "leap year: February has 29 days" do
|
||||
# 2024 is a leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly) == ~D[2024-02-29]
|
||||
|
||||
# 2023 is not a leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2023-02-01], :monthly) == ~D[2023-02-28]
|
||||
end
|
||||
|
||||
test "year boundary: December 31 to January 1" do
|
||||
# Yearly cycle
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
||||
|
||||
# Monthly cycle across year boundary
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-12-01], :monthly) == ~D[2025-01-01]
|
||||
|
||||
# Half-yearly cycle across year boundary
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :half_yearly) == ~D[2025-01-01]
|
||||
|
||||
# Quarterly cycle across year boundary
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-10-01], :quarterly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "month boundary: different month lengths" do
|
||||
# 31-day months
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :monthly) == ~D[2024-01-31]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly) == ~D[2024-03-31]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-05-01], :monthly) == ~D[2024-05-31]
|
||||
|
||||
# 30-day months
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :monthly) == ~D[2024-04-30]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-06-01], :monthly) == ~D[2024-06-30]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-09-01], :monthly) == ~D[2024-09-30]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-11-01], :monthly) == ~D[2024-11-30]
|
||||
end
|
||||
|
||||
test "date in middle of cycle: all functions work correctly" do
|
||||
middle_date = ~D[2024-03-15]
|
||||
|
||||
# calculate_cycle_start
|
||||
assert CalendarCycles.calculate_cycle_start(middle_date, :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.calculate_cycle_start(middle_date, :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(middle_date, :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(middle_date, :yearly) == ~D[2024-01-01]
|
||||
|
||||
# calculate_cycle_end
|
||||
monthly_start = CalendarCycles.calculate_cycle_start(middle_date, :monthly)
|
||||
assert CalendarCycles.calculate_cycle_end(monthly_start, :monthly) == ~D[2024-03-31]
|
||||
|
||||
# next_cycle_start
|
||||
assert CalendarCycles.next_cycle_start(monthly_start, :monthly) == ~D[2024-04-01]
|
||||
end
|
||||
|
||||
test "quarterly: all quarter boundaries correct" do
|
||||
# Q1 boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly) == ~D[2024-03-31]
|
||||
|
||||
# Q2 boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :quarterly) == ~D[2024-06-30]
|
||||
|
||||
# Q3 boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-08-15], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :quarterly) == ~D[2024-09-30]
|
||||
|
||||
# Q4 boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-11-15], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-10-01], :quarterly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "half_yearly: both half boundaries correct" do
|
||||
# First half boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly) == ~D[2024-06-30]
|
||||
|
||||
# Second half boundaries
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :half_yearly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "yearly: full year boundaries" do
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly) == ~D[2024-12-31]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue
why for API consistency?
Because all other cycle function use the current date as first argument for testing the functions. This is just kept to have consistency between the other functions.