Files
sure/app/models/period.rb
Mark Hendriksen 7f5cf4c082 Add 'all_time' period option to Period model (#279)
* Add 'all_time' period option to Period model

Introduces an 'all_time' period to the Period model, which spans from the family's oldest entry date to the current date. Includes tests to verify correct creation and date range calculation for the new period.

* Update test/models/period_test.rb

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Mark Hendriksen <hendriksen-mark@hotmail.com>

* Improve 'all_time' period fallback logic

Updates the 'all_time' period to use a 5-year fallback range when no family or entries exist, or when the oldest entry date is today. Adds tests to verify correct behavior for these edge cases.

* Update period.rb

---------

Signed-off-by: Mark Hendriksen <hendriksen-mark@hotmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2025-11-10 23:07:07 +01:00

193 lines
4.9 KiB
Ruby

class Period
include ActiveModel::Validations, Comparable
class InvalidKeyError < StandardError; end
attr_reader :key, :start_date, :end_date
validates :start_date, :end_date, presence: true, if: -> { PERIODS[key].nil? }
validates :key, presence: true, if: -> { start_date.nil? || end_date.nil? }
validate :must_be_valid_date_range
PERIODS = {
"last_day" => {
date_range: -> { [ 1.day.ago.to_date, Date.current ] },
label_short: "1D",
label: "Last Day",
comparison_label: "vs. yesterday"
},
"current_week" => {
date_range: -> { [ Date.current.beginning_of_week, Date.current ] },
label_short: "WTD",
label: "Current Week",
comparison_label: "vs. start of week"
},
"last_7_days" => {
date_range: -> { [ 7.days.ago.to_date, Date.current ] },
label_short: "7D",
label: "Last 7 Days",
comparison_label: "vs. last week"
},
"current_month" => {
date_range: -> { [ Date.current.beginning_of_month, Date.current ] },
label_short: "MTD",
label: "Current Month",
comparison_label: "vs. start of month"
},
"last_30_days" => {
date_range: -> { [ 30.days.ago.to_date, Date.current ] },
label_short: "30D",
label: "Last 30 Days",
comparison_label: "vs. last month"
},
"last_90_days" => {
date_range: -> { [ 90.days.ago.to_date, Date.current ] },
label_short: "90D",
label: "Last 90 Days",
comparison_label: "vs. last quarter"
},
"current_year" => {
date_range: -> { [ Date.current.beginning_of_year, Date.current ] },
label_short: "YTD",
label: "Current Year",
comparison_label: "vs. start of year"
},
"last_365_days" => {
date_range: -> { [ 365.days.ago.to_date, Date.current ] },
label_short: "365D",
label: "Last 365 Days",
comparison_label: "vs. 1 year ago"
},
"last_5_years" => {
date_range: -> { [ 5.years.ago.to_date, Date.current ] },
label_short: "5Y",
label: "Last 5 Years",
comparison_label: "vs. 5 years ago"
},
"last_10_years" => {
date_range: -> { [ 10.years.ago.to_date, Date.current ] },
label_short: "10Y",
label: "Last 10 Years",
comparison_label: "vs. 10 years ago"
},
"all_time" => {
date_range: -> {
oldest_date = Current.family&.oldest_entry_date
# If no family or no entries exist, use a reasonable historical fallback
# to ensure "All Time" represents a meaningful range, not just today
start_date = if oldest_date && oldest_date < Date.current
oldest_date
else
5.years.ago.to_date
end
[ start_date, Date.current ]
},
label_short: "All",
label: "All Time",
comparison_label: "vs. beginning"
}
}
class << self
def from_key(key)
unless PERIODS.key?(key)
raise InvalidKeyError, "Invalid period key: #{key}"
end
start_date, end_date = PERIODS[key].fetch(:date_range).call
new(key: key, start_date: start_date, end_date: end_date)
end
def custom(start_date:, end_date:)
new(start_date: start_date, end_date: end_date)
end
def all
PERIODS.map { |key, period| from_key(key) }
end
def as_options
all.map { |period| [ period.label_short, period.key ] }
end
end
PERIODS.each do |key, period|
define_singleton_method(key) do
from_key(key)
end
end
def initialize(start_date: nil, end_date: nil, key: nil, date_format: "%b %d, %Y")
@key = key
@start_date = start_date
@end_date = end_date
@date_format = date_format
validate!
end
def <=>(other)
[ start_date, end_date ] <=> [ other.start_date, other.end_date ]
end
def date_range
start_date..end_date
end
def days
(end_date - start_date).to_i + 1
end
def within?(other)
start_date >= other.start_date && end_date <= other.end_date
end
def interval
if days > 366
"1 week"
else
"1 day"
end
end
def label
if key_metadata
key_metadata.fetch(:label)
else
"Custom Period"
end
end
def label_short
if key_metadata
key_metadata.fetch(:label_short)
else
"Custom"
end
end
def comparison_label
if key_metadata
key_metadata.fetch(:comparison_label)
else
"#{start_date.strftime(@date_format)} to #{end_date.strftime(@date_format)}"
end
end
private
def key_metadata
@key_metadata ||= PERIODS[key]
end
def must_be_valid_date_range
return if start_date.nil? || end_date.nil?
unless start_date.is_a?(Date) && end_date.is_a?(Date)
errors.add(:start_date, "must be a valid date, got #{start_date.inspect}")
errors.add(:end_date, "must be a valid date, got #{end_date.inspect}")
return
end
errors.add(:start_date, "must be before end date") if start_date > end_date
end
end