paykit_lib/allowance/
periods.rs1use chrono::{DateTime, Datelike, NaiveDate, TimeDelta, Utc};
4
5use super::{AllowanceEvaluationBlock, AllowancePeriod, AllowancePeriodKind, AllowancePeriodUnit};
6use crate::validation::parse_utc_timestamp;
7
8type MathResult<T> = std::result::Result<T, AllowanceEvaluationBlock>;
9const OVERFLOW: AllowanceEvaluationBlock = AllowanceEvaluationBlock::ArithmeticOverflow;
10const NANOS_PER_SECOND: i128 = 1_000_000_000;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct AllowancePeriodWindow {
15 starts_at: DateTime<Utc>,
16 ends_at: DateTime<Utc>,
17 kind: AllowancePeriodKind,
18}
19
20impl AllowancePeriodWindow {
21 pub fn starts_at(&self) -> DateTime<Utc> {
23 self.starts_at
24 }
25
26 pub fn ends_at(&self) -> DateTime<Utc> {
28 self.ends_at
29 }
30
31 pub fn contains(&self, admitted_at: DateTime<Utc>) -> bool {
33 match self.kind {
34 AllowancePeriodKind::Anchored => {
35 self.starts_at <= admitted_at && admitted_at < self.ends_at
36 }
37 AllowancePeriodKind::Rolling => {
38 self.starts_at < admitted_at && admitted_at <= self.ends_at
39 }
40 }
41 }
42}
43
44pub fn allowance_period_window(
51 period: &AllowancePeriod,
52 trusted_time: DateTime<Utc>,
53) -> MathResult<AllowancePeriodWindow> {
54 timestamp_nanos(trusted_time)?;
55 let (starts_at, ends_at) = match period.kind() {
56 AllowancePeriodKind::Rolling => {
57 let duration = TimeDelta::try_seconds(fixed_seconds(period)?).ok_or(OVERFLOW)?;
58 (
59 trusted_time.checked_sub_signed(duration).ok_or(OVERFLOW)?,
60 trusted_time,
61 )
62 }
63 AllowancePeriodKind::Anchored => anchored_window(period, trusted_time)?,
64 };
65 Ok(AllowancePeriodWindow {
66 starts_at,
67 ends_at,
68 kind: period.kind(),
69 })
70}
71
72fn anchored_window(
73 period: &AllowancePeriod,
74 time: DateTime<Utc>,
75) -> MathResult<(DateTime<Utc>, DateTime<Utc>)> {
76 let anchor = parse_utc_timestamp(period.anchor().ok_or(OVERFLOW)?, "Allowance anchor")
77 .map_err(|_| OVERFLOW)?
78 .with_timezone(&Utc);
79 timestamp_nanos(anchor)?;
80 match period.unit() {
81 AllowancePeriodUnit::Month | AllowancePeriodUnit::Year => {
82 calendar_window(period, anchor, time)
83 }
84 _ => fixed_window(period, anchor, time),
85 }
86}
87
88fn fixed_seconds(period: &AllowancePeriod) -> MathResult<i64> {
89 let factor = match period.unit() {
90 AllowancePeriodUnit::Minute => 60,
91 AllowancePeriodUnit::Hour => 3600,
92 AllowancePeriodUnit::Day => 86400,
93 AllowancePeriodUnit::Week => 604800,
94 _ => return Err(OVERFLOW),
95 };
96 i64::try_from(period.every().checked_mul(factor).ok_or(OVERFLOW)?).map_err(|_| OVERFLOW)
97}
98
99fn fixed_window(
100 period: &AllowancePeriod,
101 anchor: DateTime<Utc>,
102 time: DateTime<Utc>,
103) -> MathResult<(DateTime<Utc>, DateTime<Utc>)> {
104 let length = i128::from(fixed_seconds(period)?)
105 .checked_mul(NANOS_PER_SECOND)
106 .ok_or(OVERFLOW)?;
107 let anchor = timestamp_nanos(anchor)?;
108 let k = timestamp_nanos(time)?
109 .checked_sub(anchor)
110 .ok_or(OVERFLOW)?
111 .div_euclid(length);
112 let start = anchor
113 .checked_add(k.checked_mul(length).ok_or(OVERFLOW)?)
114 .ok_or(OVERFLOW)?;
115 Ok((
116 from_nanos(start)?,
117 from_nanos(start.checked_add(length).ok_or(OVERFLOW)?)?,
118 ))
119}
120
121fn timestamp_nanos(time: DateTime<Utc>) -> MathResult<i128> {
122 if time.timestamp_subsec_nanos() >= 1_000_000_000 {
123 return Err(OVERFLOW);
124 }
125 i128::from(time.timestamp())
126 .checked_mul(NANOS_PER_SECOND)
127 .and_then(|seconds| seconds.checked_add(i128::from(time.timestamp_subsec_nanos())))
128 .ok_or(OVERFLOW)
129}
130
131fn from_nanos(value: i128) -> MathResult<DateTime<Utc>> {
132 let seconds = i64::try_from(value.div_euclid(NANOS_PER_SECOND)).map_err(|_| OVERFLOW)?;
133 let nanos = u32::try_from(value.rem_euclid(NANOS_PER_SECOND)).map_err(|_| OVERFLOW)?;
134 DateTime::from_timestamp(seconds, nanos).ok_or(OVERFLOW)
135}
136
137fn calendar_window(
138 period: &AllowancePeriod,
139 anchor: DateTime<Utc>,
140 time: DateTime<Utc>,
141) -> MathResult<(DateTime<Utc>, DateTime<Utc>)> {
142 let months = period
143 .every()
144 .checked_mul(if period.unit() == AllowancePeriodUnit::Year {
145 12
146 } else {
147 1
148 })
149 .ok_or(OVERFLOW)?;
150 let step = i64::try_from(months).map_err(|_| OVERFLOW)?;
151 let mut k = (month_index(time) - month_index(anchor)).div_euclid(step);
152 let mut start = calendar_boundary(anchor, k, step)?;
153 if start > time {
154 k = k.checked_sub(1).ok_or(OVERFLOW)?;
155 start = calendar_boundary(anchor, k, step)?;
156 }
157 Ok((
158 start,
159 calendar_boundary(anchor, k.checked_add(1).ok_or(OVERFLOW)?, step)?,
160 ))
161}
162
163fn month_index(time: DateTime<Utc>) -> i64 {
164 i64::from(time.year()) * 12 + i64::from(time.month0())
165}
166
167fn calendar_boundary(anchor: DateTime<Utc>, k: i64, step: i64) -> MathResult<DateTime<Utc>> {
168 let index = month_index(anchor)
169 .checked_add(k.checked_mul(step).ok_or(OVERFLOW)?)
170 .ok_or(OVERFLOW)?;
171 let year = i32::try_from(index.div_euclid(12)).map_err(|_| OVERFLOW)?;
172 let month = u32::try_from(index.rem_euclid(12) + 1).map_err(|_| OVERFLOW)?;
173 let day = anchor.day().min(days_in_month(year, month));
174 let date = NaiveDate::from_ymd_opt(year, month, day).ok_or(OVERFLOW)?;
175 Ok(date.and_time(anchor.time()).and_utc())
176}
177
178fn days_in_month(year: i32, month: u32) -> u32 {
179 match month {
180 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
181 2 => 28,
182 4 | 6 | 9 | 11 => 30,
183 _ => 31,
184 }
185}