Skip to content

Commit f672ba0

Browse files
committed
refactor(raw): add Timestamp wrapper around jiff::Timestamp
Introduce a new Timestamp newtype wrapper around jiff::Timestamp to provide a more ergonomic and type-safe API for time operations across OpenDAL. Key features: - Wraps jiff::Timestamp with a thin newtype pattern - Implements common conversion traits (From, TryFrom, FromStr) - Provides formatting methods (format_http_date, format_rfc3339, etc) - Adds helper constructors (now, from_millisecond, from_second) - Maintains compatibility with SystemTime and Duration This wrapper simplifies time handling and makes the API more consistent throughout the codebase while preserving all jiff functionality.
1 parent 4fe4141 commit f672ba0

1 file changed

Lines changed: 324 additions & 0 deletions

File tree

core/src/raw/time.rs

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Time related utils.
19+
20+
use crate::*;
21+
use std::fmt;
22+
use std::ops::{Add, AddAssign, Sub, SubAssign};
23+
use std::str::FromStr;
24+
use std::time::{Duration, SystemTime};
25+
26+
/// An instant in time represented as the number of nanoseconds since the Unix epoch.
27+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28+
pub struct Timestamp(jiff::Timestamp);
29+
30+
impl FromStr for Timestamp {
31+
type Err = Error;
32+
33+
/// Parse a timestamp by the default [`DateTimeParser`].
34+
///
35+
/// All of them are valid time:
36+
///
37+
/// - `2022-03-13T07:20:04Z`
38+
/// - `2022-03-01T08:12:34+00:00`
39+
/// - `2022-03-01T08:12:34.00+00:00`
40+
/// - `2022-07-08T02:14:07+02:00[Europe/Paris]`
41+
///
42+
/// [`DateTimeParser`]: jiff::fmt::temporal::DateTimeParser
43+
fn from_str(s: &str) -> Result<Self, Self::Err> {
44+
match s.parse() {
45+
Ok(t) => Ok(Timestamp(t)),
46+
Err(err) => Err(Error::new(
47+
ErrorKind::Unexpected,
48+
format!("parse '{s}' into timestamp failed"),
49+
)
50+
.set_source(err)),
51+
}
52+
}
53+
}
54+
55+
impl fmt::Display for Timestamp {
56+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57+
write!(f, "{}", self.0)
58+
}
59+
}
60+
61+
impl Timestamp {
62+
/// The minimum timestamp value.
63+
pub const MIN: Self = Self(jiff::Timestamp::MIN);
64+
65+
/// The maximum timestamp value.
66+
pub const MAX: Self = Self(jiff::Timestamp::MAX);
67+
68+
/// Create the timestamp of now.
69+
pub fn now() -> Self {
70+
Self(jiff::Timestamp::now())
71+
}
72+
73+
/// Format the timestamp into date: `20220301`
74+
pub fn format_date(self) -> String {
75+
self.0.strftime("%Y%m%d").to_string()
76+
}
77+
78+
/// Format the timestamp into ISO8601: `20220313T072004Z`
79+
pub fn format_iso8601(self) -> String {
80+
self.0.strftime("%Y%m%dT%H%M%SZ").to_string()
81+
}
82+
83+
/// Format the timestamp into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
84+
///
85+
/// ## Note
86+
///
87+
/// HTTP date is slightly different from RFC2822.
88+
///
89+
/// - Timezone is fixed to GMT.
90+
/// - Day must be 2 digit.
91+
pub fn format_http_date(self) -> String {
92+
self.0.strftime("%a, %d %b %Y %T GMT").to_string()
93+
}
94+
95+
/// Format the timestamp into RFC3339 in Zulu: `2022-03-13T07:20:04Z`
96+
pub fn format_rfc3339_zulu(self) -> String {
97+
self.0.strftime("%FT%TZ").to_string()
98+
}
99+
100+
/// Returns this timestamp as a number of seconds since the Unix epoch.
101+
///
102+
/// This only returns the number of whole seconds. That is, if there are
103+
/// any fractional seconds in this timestamp, then they are truncated.
104+
pub fn as_second(self) -> i64 {
105+
self.0.as_second()
106+
}
107+
108+
/// Returns the fractional second component of this timestamp in units of
109+
/// nanoseconds.
110+
///
111+
/// It is guaranteed that this will never return a value that is greater
112+
/// than 1 second (or less than -1 second).
113+
pub fn subsec_nanosecond(self) -> i32 {
114+
self.0.subsec_nanosecond()
115+
}
116+
117+
/// Convert to `SystemTime`.
118+
pub fn as_system_time(self) -> SystemTime {
119+
SystemTime::from(self.0)
120+
}
121+
122+
/// Creates a new instant in time from the number of milliseconds elapsed
123+
/// since the Unix epoch.
124+
///
125+
/// When `millisecond` is negative, it corresponds to an instant in time
126+
/// before the Unix epoch. A smaller number corresponds to an instant in
127+
/// time further into the past.
128+
pub fn from_millisecond(millis: i64) -> Result<Self> {
129+
match jiff::Timestamp::from_millisecond(millis) {
130+
Ok(t) => Ok(Timestamp(t)),
131+
Err(err) => Err(Error::new(
132+
ErrorKind::Unexpected,
133+
format!("convert '{millis}' milliseconds into timestamp failed"),
134+
)
135+
.set_source(err)),
136+
}
137+
}
138+
139+
/// Creates a new instant in time from the number of seconds elapsed since
140+
/// the Unix epoch.
141+
///
142+
/// When `second` is negative, it corresponds to an instant in time before
143+
/// the Unix epoch. A smaller number corresponds to an instant in time
144+
/// further into the past.
145+
pub fn from_second(second: i64) -> Result<Self> {
146+
match jiff::Timestamp::from_second(second) {
147+
Ok(t) => Ok(Timestamp(t)),
148+
Err(err) => Err(Error::new(
149+
ErrorKind::Unexpected,
150+
format!("convert '{second}' seconds into timestamp failed"),
151+
)
152+
.set_source(err)),
153+
}
154+
}
155+
156+
/// Parse a timestamp from RFC2822.
157+
///
158+
/// All of them are valid time:
159+
///
160+
/// - `Sat, 13 Jul 2024 15:09:59 -0400`
161+
/// - `Mon, 15 Aug 2022 16:50:12 GMT`
162+
pub fn parse_rfc2822(s: &str) -> Result<Timestamp> {
163+
match jiff::fmt::rfc2822::parse(s) {
164+
Ok(zoned) => Ok(Timestamp(zoned.timestamp())),
165+
Err(err) => Err(Error::new(
166+
ErrorKind::Unexpected,
167+
format!("parse '{s}' into rfc2822 failed"),
168+
)
169+
.set_source(err)),
170+
}
171+
}
172+
173+
/// Parse the string format "2023-10-31 21:59:10.000000".
174+
pub fn parse_datetime_utc(s: &str) -> Result<Timestamp> {
175+
let dt = s.parse::<jiff::civil::DateTime>().map_err(|err| {
176+
Error::new(
177+
ErrorKind::Unexpected,
178+
format!("parse '{s}' into datetime failed"),
179+
)
180+
.set_source(err)
181+
})?;
182+
183+
let ts = jiff::tz::TimeZone::UTC.to_timestamp(dt).map_err(|err| {
184+
Error::new(
185+
ErrorKind::Unexpected,
186+
format!("convert '{s}' into timestamp failed"),
187+
)
188+
.set_source(err)
189+
})?;
190+
191+
Ok(Timestamp(ts))
192+
}
193+
194+
/// Convert to inner jiff::Timestamp for compatibility.
195+
///
196+
/// This method is provided for accessing the underlying jiff::Timestamp
197+
/// when needed for interoperability with jiff-specific APIs.
198+
pub fn into_inner(self) -> jiff::Timestamp {
199+
self.0
200+
}
201+
202+
/// Convert to a Zoned datetime in the given timezone.
203+
pub fn to_zoned(self, tz: jiff::tz::TimeZone) -> jiff::Zoned {
204+
self.0.to_zoned(tz)
205+
}
206+
207+
/// Format the timestamp using `strftime` format string.
208+
///
209+
/// Common formats:
210+
/// - `"%Y-%m-%d"` - Date like `2022-03-01`
211+
/// - `"%a, %d %b %Y %H:%M:%S GMT"` - HTTP date
212+
///
213+
/// For full format documentation, see [jiff::fmt::strtime](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html)
214+
pub fn strftime(self, format: &str) -> String {
215+
self.0.strftime(format).to_string()
216+
}
217+
}
218+
219+
impl From<jiff::Timestamp> for Timestamp {
220+
fn from(t: jiff::Timestamp) -> Self {
221+
Timestamp(t)
222+
}
223+
}
224+
225+
impl TryFrom<SystemTime> for Timestamp {
226+
type Error = Error;
227+
228+
fn try_from(t: SystemTime) -> Result<Self> {
229+
jiff::Timestamp::try_from(t).map(Timestamp).map_err(|err| {
230+
Error::new(ErrorKind::Unexpected, "input timestamp overflow").set_source(err)
231+
})
232+
}
233+
}
234+
235+
impl Add<Duration> for Timestamp {
236+
type Output = Timestamp;
237+
238+
fn add(self, rhs: Duration) -> Timestamp {
239+
let ts = self
240+
.0
241+
.checked_add(rhs)
242+
.expect("adding unsigned duration to timestamp overflowed");
243+
244+
Timestamp(ts)
245+
}
246+
}
247+
248+
impl AddAssign<Duration> for Timestamp {
249+
fn add_assign(&mut self, rhs: Duration) {
250+
*self = *self + rhs
251+
}
252+
}
253+
254+
impl Sub<Duration> for Timestamp {
255+
type Output = Timestamp;
256+
257+
fn sub(self, rhs: Duration) -> Timestamp {
258+
let ts = self
259+
.0
260+
.checked_sub(rhs)
261+
.expect("subtracting unsigned duration from timestamp overflowed");
262+
263+
Timestamp(ts)
264+
}
265+
}
266+
267+
impl SubAssign<Duration> for Timestamp {
268+
fn sub_assign(&mut self, rhs: Duration) {
269+
*self = *self - rhs
270+
}
271+
}
272+
273+
#[cfg(test)]
274+
mod tests {
275+
use super::*;
276+
277+
fn test_time() -> Timestamp {
278+
Timestamp("2022-03-01T08:12:34Z".parse().unwrap())
279+
}
280+
281+
#[test]
282+
fn test_format_date() {
283+
let t = test_time();
284+
assert_eq!("20220301", t.format_date())
285+
}
286+
287+
#[test]
288+
fn test_format_iso8601() {
289+
let t = test_time();
290+
assert_eq!("20220301T081234Z", t.format_iso8601())
291+
}
292+
293+
#[test]
294+
fn test_format_http_date() {
295+
let t = test_time();
296+
assert_eq!("Tue, 01 Mar 2022 08:12:34 GMT", t.format_http_date())
297+
}
298+
299+
#[test]
300+
fn test_format_rfc3339() {
301+
let t = test_time();
302+
assert_eq!("2022-03-01T08:12:34Z", t.format_rfc3339_zulu())
303+
}
304+
305+
#[test]
306+
fn test_parse_rfc3339() {
307+
let t = test_time();
308+
309+
for v in [
310+
"2022-03-01T08:12:34Z",
311+
"2022-03-01T08:12:34+00:00",
312+
"2022-03-01T08:12:34.00+00:00",
313+
] {
314+
assert_eq!(t, v.parse().expect("must be valid time"));
315+
}
316+
}
317+
318+
#[test]
319+
fn test_parse_rfc2822() {
320+
let s = "Sat, 29 Oct 1994 19:43:31 +0000";
321+
let v = Timestamp::parse_rfc2822(s).unwrap();
322+
assert_eq!("Sat, 29 Oct 1994 19:43:31 GMT", v.format_http_date());
323+
}
324+
}

0 commit comments

Comments
 (0)