cftime_rs/
utils.rs

1//! Utils crate where common behaviour for computing dates are shared
2
3use crate::{
4    calendars::Calendar,
5    constants,
6    datetime::CFDatetime,
7    datetimes::traits::IsLeap,
8    duration::CFDuration,
9    parser::{parse_cf_time, Unit},
10};
11use std::time::Duration;
12
13/// Calculates the timestamp from the given year, month, and day.
14///
15/// # Arguments
16///
17/// * `year` - The year.
18/// * `month` - The month.
19/// * `day` - The day.
20///
21/// # Returns
22///
23/// The calculated timestamp.
24///
25/// # Errors
26///
27/// Returns an error if there was an issue calculating the timestamp.
28pub fn get_timestamp_from_ymd<T: IsLeap>(
29    year: i64,
30    month: u8,
31    day: u8,
32) -> Result<i64, crate::errors::Error> {
33    let mut timestamp: i64 = 0;
34
35    // Calculate years
36
37    let mut current_year: i64 = year;
38    loop {
39        if current_year == constants::UNIX_DEFAULT_YEAR {
40            break;
41        }
42        // We have to look at the preceding year. For example if year == 1972
43        // we have to look from 1971 to 1972
44        let year_to_look_at = current_year - (current_year > constants::UNIX_DEFAULT_YEAR) as i64;
45        let seconds_in_year: i64 = if T::is_leap(year_to_look_at) {
46            constants::SECONDS_PER_YEAR_LEAP
47        } else {
48            constants::SECONDS_PER_YEAR_NON_LEAP
49        };
50
51        if current_year > constants::UNIX_DEFAULT_YEAR {
52            timestamp += seconds_in_year;
53            current_year -= 1;
54        } else {
55            timestamp -= seconds_in_year;
56            current_year += 1;
57        }
58    }
59
60    // Calculate months
61    let mut current_month = 0;
62    loop {
63        if current_month + 1 == month {
64            break;
65        }
66        if T::is_leap(year) {
67            timestamp += constants::DAYS_PER_MONTH_LEAP[(current_month) as usize] as i64
68                * constants::SECS_PER_DAY as i64;
69        } else {
70            timestamp += constants::DAYS_PER_MONTH[(current_month) as usize] as i64
71                * constants::SECS_PER_DAY as i64;
72        }
73        current_month += 1;
74    }
75
76    // Calculate days
77    timestamp += (day as i64 - 1) * constants::SECS_PER_DAY as i64;
78
79    Ok(timestamp)
80}
81
82/// Converts a timestamp into hours, minutes, and seconds.
83///
84/// # Arguments
85///
86/// * `timestamp` - The timestamp to convert.
87///
88/// # Returns
89///
90/// A tuple containing the hours, minutes, and seconds.
91pub fn get_hms_from_timestamp(timestamp: i64) -> (u8, u8, u8) {
92    let _mod_sec = constants::SECS_PER_DAY as i64;
93    let seconds = (timestamp % constants::SECS_PER_DAY as i64 + constants::SECS_PER_DAY as i64)
94        % constants::SECS_PER_DAY as i64;
95    let sec = (seconds % 60) as u8;
96    let min = ((seconds / 60) % 60) as u8;
97    let hour = ((seconds / 3600) % 24) as u8;
98    (hour, min, sec)
99}
100/// Converts a timestamp to the year, month, day, hour, minute, and second components.
101///
102/// # Arguments
103///
104/// * `timestamp` - The timestamp to convert
105///
106/// # Generic Parameters
107///
108/// * `T` - A type that implements the `IsLeap` trait, used to determine if a year is a leap year
109///
110/// # Returns
111///
112/// A tuple containing the year, month, day, hour, minute, and second components of the timestamp.
113pub fn get_ymd_hms_from_timestamp<T: IsLeap>(timestamp: i64) -> (i64, u8, u8, u8, u8, u8) {
114    let mut remaining_timestamp = timestamp;
115    let mut current_year = constants::UNIX_DEFAULT_YEAR;
116
117    // Determine the direction (past or future)
118    let direction = if timestamp >= 0 { 1 } else { -1 };
119
120    loop {
121        let year_to_look_at = if current_year > constants::UNIX_DEFAULT_YEAR {
122            current_year
123        } else {
124            current_year - 1
125        };
126        let seconds_in_year: i64 = if T::is_leap(year_to_look_at) {
127            constants::SECONDS_PER_YEAR_LEAP
128        } else {
129            constants::SECONDS_PER_YEAR_NON_LEAP
130        };
131
132        let new_remaining = remaining_timestamp - direction * seconds_in_year;
133
134        // After UNIX epoch we can stop
135        if direction == 1 && (new_remaining < 0) {
136            break;
137        }
138        // Before UNIX epoch we substract one year if needed
139        // This ensure remaining_timestamp is positive or equals 0
140        else if direction == -1 && (new_remaining >= 0) {
141            remaining_timestamp = new_remaining;
142            current_year += direction;
143            break;
144        }
145        remaining_timestamp = new_remaining;
146        current_year += direction;
147    }
148
149    // Calculate months
150    // remaining_timestamp is positive or equals 0
151    let mut month: i64 = 0;
152    loop {
153        let days_in_month: i64 = if T::is_leap(current_year) {
154            constants::DAYS_PER_MONTH_LEAP[month as usize] as i64
155        } else {
156            constants::DAYS_PER_MONTH[month as usize] as i64
157        };
158        let seconds_in_month = days_in_month * constants::SECS_PER_DAY as i64;
159
160        if remaining_timestamp < seconds_in_month {
161            break;
162        }
163        remaining_timestamp -= seconds_in_month;
164        month += 1;
165    }
166
167    // Calculate days
168    let day = (remaining_timestamp / (constants::SECS_PER_DAY as i64)) as u8;
169
170    let (hour, min, sec) = get_hms_from_timestamp(remaining_timestamp);
171    (current_year, month as u8 + 1, day + 1, hour, min, sec)
172}
173
174/// Determines if a given year is a leap year according to the Gregorian calendar.
175///
176/// # Arguments
177///
178/// * `year` - The year to be checked.
179///
180/// # Returns
181///
182/// Returns `true` if the year is a leap year, `false` otherwise.
183pub fn is_leap_gregorian(year: i64) -> bool {
184    // Optimization : Adds 1 for negative years, 0 for non-negative years
185    // We extract the sign bit from the year i64 variable
186    let f_year = ((year >> 63) & 1) + year;
187    (f_year % 400 == 0) || ((f_year % 4 == 0) && (f_year % 100 != 0))
188}
189
190/// Determines if a given year is a leap year in the Julian calendar.
191///
192/// # Arguments
193///
194/// * `year` - The year to check for leapness.
195///
196/// # Returns
197///
198/// * `true` if the year is a leap year, `false` otherwise.
199pub fn is_leap_julian(year: i64) -> bool {
200    // Optimization : Adds 1 for negative years, 0 for non-negative years
201    // We extract the sign bit from the year i64 variable
202    (((year >> 63) & 1) + year) % 4 == 0
203}
204
205fn extract_seconds_and_nanoseconds(seconds: f32) -> (u64, u32) {
206    let duration = Duration::from_secs_f32(seconds);
207    let secs = duration.as_secs();
208    let nanosecs = duration.subsec_nanos();
209
210    (secs, nanosecs)
211}
212
213/// Converts the given hour, minute, and second values into a timestamp.
214///
215/// # Arguments
216///
217/// * `hour` - The hour value (0-23).
218/// * `min` - The minute value (0-59).
219/// * `sec` - The second value (0.0-59.999...).
220///
221/// # Returns
222///
223/// A tuple containing the total number of seconds and the number of nanoseconds.
224///
225/// # Errors
226///
227/// Returns an error if any of the input values are out of bounds.
228pub fn get_timestamp_from_hms(
229    hour: u8,
230    min: u8,
231    sec: f32,
232) -> Result<(i64, u32), crate::errors::Error> {
233    if hour > 23 {
234        return Err(crate::errors::Error::InvalidTime(
235            format!("Hour {hour} is out of bounds").to_string(),
236        ));
237    }
238    if min > 59 {
239        return Err(crate::errors::Error::InvalidTime(
240            format!("Minute {min} is out of bounds").to_string(),
241        ));
242    }
243    if !(0.0..60.0).contains(&sec) {
244        return Err(crate::errors::Error::InvalidTime(
245            format!("Second {sec} is out of bounds").to_string(),
246        ));
247    }
248    let (round_seconds, nanoseconds) = extract_seconds_and_nanoseconds(sec);
249    let total_seconds = (hour as u32 * constants::SECS_PER_HOUR
250        + min as u32 * constants::SECS_PER_MINUTE
251        + round_seconds as u32)
252        % constants::SECS_PER_DAY;
253
254    Ok((total_seconds as i64, nanoseconds))
255}
256
257pub fn get_datetime_and_unit_from_units(
258    units: &str,
259    calendar: Calendar,
260) -> Result<(CFDatetime, Unit), crate::errors::Error> {
261    let parsed_cf_time = parse_cf_time(units)?;
262    let (year, month, day) = parsed_cf_time.datetime.ymd;
263    let (hour, minute, second) = match parsed_cf_time.datetime.hms {
264        Some(hms) => (hms.0, hms.1, hms.2),
265        None => (0, 0, 0.0),
266    };
267    let cf_datetime = CFDatetime::from_ymd_hms(year, month, day, hour, minute, second, calendar)?;
268    let unit = parsed_cf_time.unit;
269    Ok((cf_datetime, unit))
270}
271/// Normalize the given number of nanoseconds into seconds and remaining nanoseconds.
272///
273/// # Arguments
274///
275/// * `nanoseconds` - The number of nanoseconds to normalize.
276///
277/// # Returns
278///
279/// A tuple containing the remaining seconds and remaining nanoseconds.
280///
281/// # Examples
282///
283/// ```
284/// use cftime_rs::utils::normalize_nanoseconds;
285/// let nanoseconds = 1_500_000_000;
286/// let (seconds, remaining_nanoseconds) = normalize_nanoseconds(nanoseconds);
287/// assert_eq!(seconds, 1);
288/// assert_eq!(remaining_nanoseconds, 500_000_000);
289/// ```
290///
291/// ```
292/// use cftime_rs::utils::normalize_nanoseconds;
293/// let nanoseconds = -2_500_000_000;
294/// let (seconds, remaining_nanoseconds) = normalize_nanoseconds(nanoseconds);
295/// assert_eq!(seconds, -3);
296/// assert_eq!(remaining_nanoseconds, 500_000_000);
297/// ```
298pub fn normalize_nanoseconds(nanoseconds: i64) -> (i64, u32) {
299    // Calculate the number of remaining seconds
300    let mut remaining_seconds = nanoseconds / 1e9 as i64;
301
302    // Calculate the number of remaining nanoseconds
303    let remaining_nanoseconds: i64 = if remaining_seconds < 0 {
304        // If the remaining seconds is negative, subtract 1 and calculate the remaining nanoseconds accordingly
305        remaining_seconds -= 1;
306        (nanoseconds + (remaining_seconds.abs() * 1_000_000_000)) % 1_000_000_000
307    } else {
308        // If the remaining seconds is positive or zero, calculate the remaining nanoseconds directly
309        nanoseconds % 1e9 as i64
310    };
311    (remaining_seconds, remaining_nanoseconds as u32)
312}
313
314/// Converts a unit of time to its corresponding encoded value.
315///
316/// # Arguments
317///
318/// * `unit` - The unit of time to encode.
319/// * `duration` - The duration to encode.
320///
321/// # Returns
322///
323/// The encoded value of the unit of time.
324pub fn unit_to_encode(unit: &Unit, duration: CFDuration) -> f64 {
325    match unit {
326        Unit::Year => duration.num_years(),     // Convert to years
327        Unit::Month => duration.num_months(),   // Convert to months
328        Unit::Day => duration.num_days(),       // Convert to days
329        Unit::Hour => duration.num_hours(),     // Convert to hours
330        Unit::Minute => duration.num_minutes(), // Convert to minutes
331        Unit::Second => duration.num_seconds(), // Convert to seconds
332        Unit::Millisecond => duration.num_milliseconds(), // Convert to milliseconds
333        Unit::Microsecond => duration.num_microseconds(), // Convert to microseconds
334        Unit::Nanosecond => duration.num_nanoseconds(), // Convert to nanoseconds
335    }
336}