pdf_rs/
date.rs

1use std::ops::Range;
2use std::str::FromStr;
3use crate::error::PDFError;
4
5/// Represents a date and time value used in PDF documents.
6///
7/// This struct stores time information with millisecond precision,
8/// following the PDF specification for date/time representation.
9pub struct Date {
10    /// Time zone offset from UTC in hours.
11    pub(crate) time_zero: i8,
12    /// Milliseconds component of the time.
13    pub(crate) millisecond: u64,
14}
15
16
17
18impl Date {
19    /// Creates a new Date instance with the specified date and time components.
20    ///
21    /// # Arguments
22    ///
23    /// * `year` - The year (e.g., 2024)
24    /// * `month` - The month (1-12)
25    /// * `day` - The day of the month (1-31)
26    /// * `hour` - The hour (0-23)
27    /// * `minute` - The minute (0-59)
28    /// * `second` - The second (0-59)
29    /// * `time_zero` - Time zone offset from UTC in hours (-12 to +12)
30    /// * `utm` - shall be the absolute value of the offset from UT in minutes (00–59)
31    ///
32    /// # Returns
33    ///
34    /// A new Date instance with the calculated Unix timestamp in milliseconds
35    pub fn new(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8, time_zero: i8, utm: u8) -> Self {
36        let millisecond = Self::calculate_unix_timestamp_millis(year, month, day, hour, minute, second, time_zero, utm);
37
38        Date {
39            time_zero,
40            millisecond,
41        }
42    }
43
44    /// Calculates the Unix timestamp in milliseconds from 1970-01-01 00:00:00 UTC.
45    ///
46    /// This function computes the number of milliseconds elapsed since the Unix epoch
47    /// (January 1, 1970, 00:00:00 UTC) for the given date and time, taking into account
48    /// the time zone offset.
49    ///
50    /// # Arguments
51    ///
52    /// * `year` - The year (e.g., 2024)
53    /// * `month` - The month (1-12)
54    /// * `day` - The day of the month (1-31)
55    /// * `hour` - The hour (0-23)
56    /// * `minute` - The minute (0-59)
57    /// * `second` - The second (0-59)
58    /// * `time_zero` - Time zone offset from UTC in hours (-12 to +12)
59    /// * `utm` - Milliseconds component (0-999)
60    ///
61    /// # Returns
62    ///
63    /// The Unix timestamp in milliseconds
64    fn calculate_unix_timestamp_millis(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8, time_zero: i8, utm: u8) -> u64 {
65        // Days in each month for non-leap years
66       static DAYS_IN_MONTH: [u64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
67
68        // Calculate total days from 1970 to the given year
69        let mut total_days: u64 = 0;
70
71        // Add days for full years from 1970 to year-1
72        for y in 1970..year {
73            total_days += if Self::is_leap_year(y) { 366 } else { 365 };
74        }
75
76        // Add days for full months in the current year
77        for m in 1..month {
78            let month_idx = (m - 1) as usize;
79            total_days += DAYS_IN_MONTH[month_idx];
80            // Add leap day for February in leap years
81            if m == 2 && Self::is_leap_year(year) {
82                total_days += 1;
83            }
84        }
85
86        // Add days in the current month
87        total_days += (day - 1) as u64;
88
89        // Convert days to seconds
90        let mut total_seconds = total_days * 86400;
91
92        // Add hours, minutes, and seconds
93        total_seconds += hour as u64 * 3600;
94        total_seconds += minute as u64 * 60;
95        total_seconds += second as u64;
96
97        // Adjust for time zone offset (convert hours to seconds)
98        let tz_offset_seconds = (time_zero as i64) * 3600;
99        total_seconds = (total_seconds as i64 - tz_offset_seconds) as u64;
100
101        // Convert to milliseconds and add the milliseconds component
102        total_seconds * 1000 + (utm as u64) * 60 * 1000
103    }
104
105    /// Determines if a given year is a leap year.
106    ///
107    /// A year is a leap year if:
108    /// - It is divisible by 4, and
109    /// - It is not divisible by 100, unless it is also divisible by 400
110    ///
111    /// # Arguments
112    ///
113    /// * `year` - The year to check
114    ///
115    /// # Returns
116    ///
117    /// true if the year is a leap year, false otherwise
118    fn is_leap_year(year: i32) -> bool {
119        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
120    }
121
122    /// Returns the millisecond component of the date and time.
123    ///
124    /// This represents the number of milliseconds elapsed since the Unix epoch
125    /// (January 1, 1970, 00:00:00 UTC), adjusted for the time zone offset.
126    ///
127    /// # Returns
128    ///
129    /// The millisecond value as a `u64`.
130    pub fn get_millisecond(&self) -> u64 {
131        self.millisecond
132    }
133
134    /// Returns the time zone offset from UTC in hours.
135    ///
136    /// This represents the time zone offset that was used when creating this
137    /// `Date` instance. The value is in the range of -12 to +12 hours.
138    ///
139    /// # Returns
140    ///
141    /// The time zone offset in hours as an `i8`.
142    pub fn get_time_zero(&self) -> i8 {
143        self.time_zero
144    }
145}
146
147fn parse_part(text: &str, range: Range<usize>) -> u8 {
148    text.get(range)
149        .and_then(|s| s.parse::<u8>().ok())
150        .unwrap_or(0)
151}
152
153impl FromStr for Date {
154    type Err = PDFError;
155
156    fn from_str(text: &str) -> Result<Self, Self::Err> {
157        let length = text.len();
158        if !text.starts_with("D:") || length < 6 {
159            return Err(PDFError::IllegalDateFormat(text.to_string()));
160        }
161        let year = text[2..6].parse::<i32>().unwrap_or(0);
162        let month = parse_part(text, 6..8);
163        let day = parse_part(text, 8..10);
164        let hour = parse_part(text, 10..12);
165        let minute = parse_part(text, 12..14);
166        let second = parse_part(text, 14..16);
167        let (tz, utm) = if length >= 17 {
168            let tmp = &text[16..17];
169            let mut index = 17;
170            let time_zero = if tmp == "Z" {
171                0
172            } else {
173                let plus_sign = tmp == "+";
174                let minus_sign = tmp == "-";
175                if !plus_sign || !minus_sign || length < 19 {
176                    return Err(PDFError::IllegalDateFormat(text.to_string()));
177                }
178                let tz = parse_part(text, 17..19) as i8;
179                index = 19;
180                if minus_sign {
181                    -tz
182                } else {
183                    tz
184                }
185            };
186            if length > index && index + 3 != length {
187                return Err(PDFError::IllegalDateFormat(text.to_string()));
188            }
189            let utm = parse_part(text, index + 1..length);
190            (time_zero, utm)
191        } else {
192            (0, 0)
193        };
194        Ok(Self::new(year, month, day, hour, minute, second, tz, utm))
195    }
196
197}