use core::time::Duration as UnsignedDuration;
use crate::{
duration::{Duration, SDuration},
error::{
timestamp::Error as E, unit::UnitConfigError, Error, ErrorContext,
},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
},
shared::util::itime::ITimestamp,
tz::{Offset, TimeZone},
util::{b, constant, round::Increment},
zoned::Zoned,
RoundMode, SignedDuration, Span, SpanRound, Unit,
};
#[derive(Clone, Copy)]
pub struct Timestamp {
dur: SignedDuration,
}
impl Timestamp {
pub const MIN: Timestamp =
Timestamp { dur: SignedDuration::new(b::UnixSeconds::MIN, 0) };
pub const MAX: Timestamp = Timestamp {
dur: SignedDuration::new(
b::UnixSeconds::MAX,
b::SignedSubsecNanosecond::MAX,
),
};
pub const UNIX_EPOCH: Timestamp = Timestamp { dur: SignedDuration::ZERO };
#[cfg(feature = "std")]
pub fn now() -> Timestamp {
Timestamp::try_from(crate::now::system_time())
.expect("system time is valid")
}
#[inline]
pub fn new(second: i64, nanosecond: i32) -> Result<Timestamp, Error> {
let secs = b::UnixSeconds::check(second)?;
let nanos = b::SignedSubsecNanosecond::check(nanosecond)?;
if secs == b::UnixSeconds::MIN && nanos < 0 {
return Err(b::UnixSeconds::error().into());
}
let dur = SignedDuration::new(secs, nanos);
Ok(Timestamp { dur })
}
#[inline]
pub const fn constant(mut second: i64, mut nanosecond: i32) -> Timestamp {
second = constant::unwrapr!(
b::UnixSeconds::checkc(second),
"seconds out of range for `jiff::Timestamp`",
);
nanosecond = constant::unwrapr!(
b::SignedSubsecNanosecond::checkc(nanosecond as i64),
"nanoseconds out of range of `jiff::Timestamp`",
);
if second == b::UnixSeconds::MIN && nanosecond < 0 {
panic!("nanoseconds must be >=0 when seconds are minimal");
}
let dur = SignedDuration::new(second, nanosecond);
Timestamp { dur }
}
#[inline]
pub fn from_second(second: i64) -> Result<Timestamp, Error> {
let second = b::UnixSeconds::check(second)?;
let dur = SignedDuration::from_secs(second);
Ok(Timestamp { dur })
}
#[inline]
pub fn from_millisecond(millisecond: i64) -> Result<Timestamp, Error> {
let millisecond = b::UnixMilliseconds::check(millisecond)?;
let dur = SignedDuration::from_millis(millisecond);
Ok(Timestamp { dur })
}
#[inline]
pub fn from_microsecond(microsecond: i64) -> Result<Timestamp, Error> {
let microsecond = b::UnixMicroseconds::check(microsecond)?;
let dur = SignedDuration::from_micros(microsecond);
Ok(Timestamp { dur })
}
#[inline]
pub fn from_nanosecond(nanosecond: i128) -> Result<Timestamp, Error> {
let dur = SignedDuration::try_from_nanos_i128(nanosecond)
.ok_or_else(|| b::SpecialBoundsError::UnixNanoseconds)?;
b::UnixSeconds::check(dur.as_secs())?;
Ok(Timestamp { dur })
}
#[inline]
pub fn from_duration(
duration: SignedDuration,
) -> Result<Timestamp, Error> {
let dur = duration;
b::UnixSeconds::check(duration.as_secs())?;
if dur.as_secs() == b::UnixSeconds::MIN && dur.subsec_nanos() < 0 {
return Err(b::UnixSeconds::error().into());
}
Ok(Timestamp { dur })
}
#[inline]
pub fn as_second(self) -> i64 {
self.dur.as_secs()
}
#[inline]
pub fn as_millisecond(self) -> i64 {
let millis = self.dur.as_secs() * b::MILLIS_PER_SEC;
millis + i64::from(self.dur.subsec_millis())
}
#[inline]
pub fn as_microsecond(self) -> i64 {
let millis = self.dur.as_secs() * b::MICROS_PER_SEC;
millis + i64::from(self.dur.subsec_micros())
}
#[inline]
pub fn as_nanosecond(self) -> i128 {
self.dur.as_nanos()
}
#[inline]
pub fn subsec_millisecond(self) -> i32 {
self.dur.subsec_millis()
}
#[inline]
pub fn subsec_microsecond(self) -> i32 {
self.dur.subsec_micros()
}
#[inline]
pub fn subsec_nanosecond(self) -> i32 {
self.dur.subsec_nanos()
}
#[inline]
pub fn as_duration(self) -> SignedDuration {
self.dur
}
#[inline]
pub fn signum(self) -> i8 {
self.dur.signum()
}
#[inline]
pub fn is_zero(self) -> bool {
self.dur.is_zero()
}
#[inline]
pub fn in_tz(self, time_zone_name: &str) -> Result<Zoned, Error> {
let tz = crate::tz::db().get(time_zone_name)?;
Ok(self.to_zoned(tz))
}
#[inline]
pub fn to_zoned(self, tz: TimeZone) -> Zoned {
Zoned::new(self, tz)
}
#[inline]
pub fn checked_add<A: Into<TimestampArithmetic>>(
self,
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
duration.checked_add(self)
}
#[inline]
fn checked_add_span(self, span: &Span) -> Result<Timestamp, Error> {
if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
return Err(err);
}
if span.is_zero() {
return Ok(self);
}
if self.subsec_nanosecond() == 0 && !span.has_fractional_seconds() {
let span_seconds = span.to_hms_seconds();
let time_seconds = self.as_second();
let sum = b::UnixSeconds::checked_add(span_seconds, time_seconds)
.context(E::OverflowAddSpan)?;
return Ok(Timestamp { dur: SignedDuration::from_secs(sum) });
}
let sum = self
.as_duration()
.checked_add(span.to_invariant_duration())
.ok_or(E::OverflowAddSpan)?;
Timestamp::from_duration(sum)
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Timestamp, Error> {
let start = self.as_duration();
let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?;
Timestamp::from_duration(end)
}
#[inline]
pub fn checked_sub<A: Into<TimestampArithmetic>>(
self,
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
duration.checked_neg().and_then(|ta| ta.checked_add(self))
}
#[inline]
pub fn saturating_add<A: Into<TimestampArithmetic>>(
self,
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
duration.saturating_add(self)
}
#[inline]
pub fn saturating_sub<A: Into<TimestampArithmetic>>(
self,
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
let Ok(duration) = duration.checked_neg() else {
return Ok(Timestamp::MIN);
};
self.saturating_add(duration)
}
#[inline]
pub fn until<A: Into<TimestampDifference>>(
self,
other: A,
) -> Result<Span, Error> {
let args: TimestampDifference = other.into();
let span = args.until_with_largest_unit(self)?;
if args.rounding_may_change_span() {
span.round(args.round)
} else {
Ok(span)
}
}
#[inline]
pub fn since<A: Into<TimestampDifference>>(
self,
other: A,
) -> Result<Span, Error> {
let args: TimestampDifference = other.into();
let span = -args.until_with_largest_unit(self)?;
if args.rounding_may_change_span() {
span.round(args.round)
} else {
Ok(span)
}
}
#[inline]
pub fn duration_until(self, other: Timestamp) -> SignedDuration {
SignedDuration::timestamp_until(self, other)
}
#[inline]
pub fn duration_since(self, other: Timestamp) -> SignedDuration {
SignedDuration::timestamp_until(other, self)
}
#[inline]
pub fn round<R: Into<TimestampRound>>(
self,
options: R,
) -> Result<Timestamp, Error> {
let options: TimestampRound = options.into();
options.round(self)
}
#[inline]
pub fn series(self, period: Span) -> TimestampSeries {
TimestampSeries::new(self, period)
}
}
impl Timestamp {
#[inline]
pub fn strptime(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<Timestamp, Error> {
fmt::strtime::parse(format, input).and_then(|tm| tm.to_timestamp())
}
#[inline]
pub fn strftime<'f, F: 'f + ?Sized + AsRef<[u8]>>(
&self,
format: &'f F,
) -> fmt::strtime::Display<'f> {
fmt::strtime::Display { fmt: format.as_ref(), tm: (*self).into() }
}
#[inline]
pub fn display_with_offset(
&self,
offset: Offset,
) -> TimestampDisplayWithOffset {
TimestampDisplayWithOffset { timestamp: *self, offset }
}
}
impl Timestamp {
#[inline]
pub(crate) const fn from_itimestamp_const(its: ITimestamp) -> Timestamp {
Timestamp {
dur: SignedDuration::new_without_nano_overflow(
its.second,
its.nanosecond,
),
}
}
#[inline]
pub(crate) const fn to_itimestamp_const(&self) -> ITimestamp {
ITimestamp {
second: self.dur.as_secs(),
nanosecond: self.dur.subsec_nanos(),
}
}
}
impl Default for Timestamp {
#[inline]
fn default() -> Timestamp {
Timestamp::UNIX_EPOCH
}
}
impl core::fmt::Debug for Timestamp {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(self, f)
}
}
impl core::fmt::Display for Timestamp {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
let precision =
f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX));
temporal::DateTimePrinter::new()
.precision(precision)
.print_timestamp(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for Timestamp {
type Err = Error;
#[inline]
fn from_str(string: &str) -> Result<Timestamp, Error> {
DEFAULT_DATETIME_PARSER.parse_timestamp(string)
}
}
impl Eq for Timestamp {}
impl PartialEq for Timestamp {
#[inline]
fn eq(&self, rhs: &Timestamp) -> bool {
self.dur == rhs.dur
}
}
impl Ord for Timestamp {
#[inline]
fn cmp(&self, rhs: &Timestamp) -> core::cmp::Ordering {
self.dur.cmp(&rhs.dur)
}
}
impl PartialOrd for Timestamp {
#[inline]
fn partial_cmp(&self, rhs: &Timestamp) -> Option<core::cmp::Ordering> {
Some(self.cmp(rhs))
}
}
impl core::hash::Hash for Timestamp {
#[inline]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.dur.hash(state);
}
}
impl core::ops::Add<Span> for Timestamp {
type Output = Timestamp;
#[inline]
fn add(self, rhs: Span) -> Timestamp {
self.checked_add_span(&rhs).expect("adding span to timestamp failed")
}
}
impl core::ops::AddAssign<Span> for Timestamp {
#[inline]
fn add_assign(&mut self, rhs: Span) {
*self = *self + rhs
}
}
impl core::ops::Sub<Span> for Timestamp {
type Output = Timestamp;
#[inline]
fn sub(self, rhs: Span) -> Timestamp {
self.checked_add_span(&rhs.negate())
.expect("subtracting span from timestamp failed")
}
}
impl core::ops::SubAssign<Span> for Timestamp {
#[inline]
fn sub_assign(&mut self, rhs: Span) {
*self = *self - rhs
}
}
impl core::ops::Sub for Timestamp {
type Output = Span;
#[inline]
fn sub(self, rhs: Timestamp) -> Span {
self.since(rhs).expect("since never fails when given Timestamp")
}
}
impl core::ops::Add<SignedDuration> for Timestamp {
type Output = Timestamp;
#[inline]
fn add(self, rhs: SignedDuration) -> Timestamp {
self.checked_add_duration(rhs)
.expect("adding signed duration to timestamp overflowed")
}
}
impl core::ops::AddAssign<SignedDuration> for Timestamp {
#[inline]
fn add_assign(&mut self, rhs: SignedDuration) {
*self = *self + rhs
}
}
impl core::ops::Sub<SignedDuration> for Timestamp {
type Output = Timestamp;
#[inline]
fn sub(self, rhs: SignedDuration) -> Timestamp {
let rhs = rhs
.checked_neg()
.expect("signed duration negation resulted in overflow");
self.checked_add_duration(rhs)
.expect("subtracting signed duration from timestamp overflowed")
}
}
impl core::ops::SubAssign<SignedDuration> for Timestamp {
#[inline]
fn sub_assign(&mut self, rhs: SignedDuration) {
*self = *self - rhs
}
}
impl core::ops::Add<UnsignedDuration> for Timestamp {
type Output = Timestamp;
#[inline]
fn add(self, rhs: UnsignedDuration) -> Timestamp {
self.checked_add(rhs)
.expect("adding unsigned duration to timestamp overflowed")
}
}
impl core::ops::AddAssign<UnsignedDuration> for Timestamp {
#[inline]
fn add_assign(&mut self, rhs: UnsignedDuration) {
*self = *self + rhs
}
}
impl core::ops::Sub<UnsignedDuration> for Timestamp {
type Output = Timestamp;
#[inline]
fn sub(self, rhs: UnsignedDuration) -> Timestamp {
self.checked_sub(rhs)
.expect("subtracting unsigned duration from timestamp overflowed")
}
}
impl core::ops::SubAssign<UnsignedDuration> for Timestamp {
#[inline]
fn sub_assign(&mut self, rhs: UnsignedDuration) {
*self = *self - rhs
}
}
impl From<Zoned> for Timestamp {
#[inline]
fn from(zdt: Zoned) -> Timestamp {
zdt.timestamp()
}
}
impl<'a> From<&'a Zoned> for Timestamp {
#[inline]
fn from(zdt: &'a Zoned) -> Timestamp {
zdt.timestamp()
}
}
#[cfg(feature = "std")]
impl From<Timestamp> for std::time::SystemTime {
#[inline]
fn from(time: Timestamp) -> std::time::SystemTime {
let unix_epoch = std::time::SystemTime::UNIX_EPOCH;
let sdur = time.as_duration();
let dur = sdur.unsigned_abs();
if sdur.is_negative() {
unix_epoch.checked_sub(dur).expect("duration too big (negative)")
} else {
unix_epoch.checked_add(dur).expect("duration too big (positive)")
}
}
}
#[cfg(feature = "std")]
impl TryFrom<std::time::SystemTime> for Timestamp {
type Error = Error;
#[inline]
fn try_from(
system_time: std::time::SystemTime,
) -> Result<Timestamp, Error> {
let unix_epoch = std::time::SystemTime::UNIX_EPOCH;
let dur = SignedDuration::system_until(unix_epoch, system_time)?;
Timestamp::from_duration(dur)
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for Timestamp {
#[inline]
fn serialize<S: serde_core::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for Timestamp {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<Timestamp, D::Error> {
use serde_core::de;
struct TimestampVisitor;
impl<'de> de::Visitor<'de> for TimestampVisitor {
type Value = Timestamp;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("a timestamp string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<Timestamp, E> {
DEFAULT_DATETIME_PARSER
.parse_timestamp(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(
self,
value: &str,
) -> Result<Timestamp, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(TimestampVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Timestamp {
fn arbitrary(g: &mut quickcheck::Gen) -> Timestamp {
let secs = b::UnixSeconds::arbitrary(g);
let mut nanos = b::SignedSubsecNanosecond::arbitrary(g);
if secs == b::UnixSeconds::MIN && nanos < 0 {
nanos = 0;
}
Timestamp::new(secs, nanos).unwrap_or_default()
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Self>> {
let secs = self.as_second();
let nanos = self.subsec_nanosecond();
alloc::boxed::Box::new((secs, nanos).shrink().filter_map(
|(secs, nanos)| {
let secs = b::UnixSeconds::check(secs).ok()?;
let nanos = b::SignedSubsecNanosecond::check(nanos).ok()?;
if secs == b::UnixSeconds::MIN && nanos > 0 {
None
} else {
Timestamp::new(secs, nanos).ok()
}
},
))
}
}
#[derive(Clone, Copy, Debug)]
pub struct TimestampDisplayWithOffset {
timestamp: Timestamp,
offset: Offset,
}
impl core::fmt::Display for TimestampDisplayWithOffset {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
let precision =
f.precision().map(|p| u8::try_from(p).unwrap_or(u8::MAX));
temporal::DateTimePrinter::new()
.precision(precision)
.print_timestamp_with_offset(
&self.timestamp,
self.offset,
StdFmtWrite(f),
)
.map_err(|_| core::fmt::Error)
}
}
#[derive(Clone, Debug)]
pub struct TimestampSeries {
ts: Timestamp,
duration: Option<SignedDuration>,
}
impl TimestampSeries {
#[inline]
fn new(ts: Timestamp, period: Span) -> TimestampSeries {
let duration = SignedDuration::try_from(period).ok();
TimestampSeries { ts, duration }
}
}
impl Iterator for TimestampSeries {
type Item = Timestamp;
#[inline]
fn next(&mut self) -> Option<Timestamp> {
let duration = self.duration?;
let this = self.ts;
self.ts = self.ts.checked_add_duration(duration).ok()?;
Some(this)
}
}
impl core::iter::FusedIterator for TimestampSeries {}
#[derive(Clone, Copy, Debug)]
pub struct TimestampArithmetic {
duration: Duration,
}
impl TimestampArithmetic {
#[inline]
fn checked_add(self, ts: Timestamp) -> Result<Timestamp, Error> {
match self.duration.to_signed()? {
SDuration::Span(span) => ts.checked_add_span(span),
SDuration::Absolute(sdur) => ts.checked_add_duration(sdur),
}
}
#[inline]
fn saturating_add(self, ts: Timestamp) -> Result<Timestamp, Error> {
let Ok(signed) = self.duration.to_signed() else {
return Ok(Timestamp::MAX);
};
let result = match signed {
SDuration::Span(span) => {
if let Some(err) = span.smallest_non_time_non_zero_unit_error()
{
return Err(err);
}
ts.checked_add_span(span)
}
SDuration::Absolute(sdur) => ts.checked_add_duration(sdur),
};
Ok(result.unwrap_or_else(|_| {
if self.is_negative() {
Timestamp::MIN
} else {
Timestamp::MAX
}
}))
}
#[inline]
fn checked_neg(self) -> Result<TimestampArithmetic, Error> {
let duration = self.duration.checked_neg()?;
Ok(TimestampArithmetic { duration })
}
#[inline]
fn is_negative(&self) -> bool {
self.duration.is_negative()
}
}
impl From<Span> for TimestampArithmetic {
fn from(span: Span) -> TimestampArithmetic {
let duration = Duration::from(span);
TimestampArithmetic { duration }
}
}
impl From<SignedDuration> for TimestampArithmetic {
fn from(sdur: SignedDuration) -> TimestampArithmetic {
let duration = Duration::from(sdur);
TimestampArithmetic { duration }
}
}
impl From<UnsignedDuration> for TimestampArithmetic {
fn from(udur: UnsignedDuration) -> TimestampArithmetic {
let duration = Duration::from(udur);
TimestampArithmetic { duration }
}
}
impl<'a> From<&'a Span> for TimestampArithmetic {
fn from(span: &'a Span) -> TimestampArithmetic {
TimestampArithmetic::from(*span)
}
}
impl<'a> From<&'a SignedDuration> for TimestampArithmetic {
fn from(sdur: &'a SignedDuration) -> TimestampArithmetic {
TimestampArithmetic::from(*sdur)
}
}
impl<'a> From<&'a UnsignedDuration> for TimestampArithmetic {
fn from(udur: &'a UnsignedDuration) -> TimestampArithmetic {
TimestampArithmetic::from(*udur)
}
}
#[derive(Clone, Copy, Debug)]
pub struct TimestampDifference {
timestamp: Timestamp,
round: SpanRound<'static>,
}
impl TimestampDifference {
#[inline]
pub fn new(timestamp: Timestamp) -> TimestampDifference {
let round = SpanRound::new().mode(RoundMode::Trunc);
TimestampDifference { timestamp, round }
}
#[inline]
pub fn smallest(self, unit: Unit) -> TimestampDifference {
TimestampDifference { round: self.round.smallest(unit), ..self }
}
#[inline]
pub fn largest(self, unit: Unit) -> TimestampDifference {
TimestampDifference { round: self.round.largest(unit), ..self }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> TimestampDifference {
TimestampDifference { round: self.round.mode(mode), ..self }
}
#[inline]
pub fn increment(self, increment: i64) -> TimestampDifference {
TimestampDifference { round: self.round.increment(increment), ..self }
}
#[inline]
fn rounding_may_change_span(&self) -> bool {
self.round.rounding_may_change_span()
}
#[inline]
fn until_with_largest_unit(&self, t1: Timestamp) -> Result<Span, Error> {
let t2 = self.timestamp;
let largest = self
.round
.get_largest()
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Second));
if largest >= Unit::Day {
return Err(Error::from(
UnitConfigError::RoundToUnitUnsupported { unit: largest },
));
}
let diff = t2.as_duration() - t1.as_duration();
Span::from_invariant_duration(largest, diff)
}
}
impl From<Timestamp> for TimestampDifference {
#[inline]
fn from(ts: Timestamp) -> TimestampDifference {
TimestampDifference::new(ts)
}
}
impl From<Zoned> for TimestampDifference {
#[inline]
fn from(zdt: Zoned) -> TimestampDifference {
TimestampDifference::new(Timestamp::from(zdt))
}
}
impl<'a> From<&'a Zoned> for TimestampDifference {
#[inline]
fn from(zdt: &'a Zoned) -> TimestampDifference {
TimestampDifference::from(Timestamp::from(zdt))
}
}
impl From<(Unit, Timestamp)> for TimestampDifference {
#[inline]
fn from((largest, ts): (Unit, Timestamp)) -> TimestampDifference {
TimestampDifference::from(ts).largest(largest)
}
}
impl From<(Unit, Zoned)> for TimestampDifference {
#[inline]
fn from((largest, zdt): (Unit, Zoned)) -> TimestampDifference {
TimestampDifference::from((largest, Timestamp::from(zdt)))
}
}
impl<'a> From<(Unit, &'a Zoned)> for TimestampDifference {
#[inline]
fn from((largest, zdt): (Unit, &'a Zoned)) -> TimestampDifference {
TimestampDifference::from((largest, Timestamp::from(zdt)))
}
}
#[derive(Clone, Copy, Debug)]
pub struct TimestampRound {
smallest: Unit,
mode: RoundMode,
increment: i64,
}
impl TimestampRound {
#[inline]
pub fn new() -> TimestampRound {
TimestampRound {
smallest: Unit::Nanosecond,
mode: RoundMode::HalfExpand,
increment: 1,
}
}
#[inline]
pub fn smallest(self, unit: Unit) -> TimestampRound {
TimestampRound { smallest: unit, ..self }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> TimestampRound {
TimestampRound { mode, ..self }
}
#[inline]
pub fn increment(self, increment: i64) -> TimestampRound {
TimestampRound { increment, ..self }
}
pub(crate) fn round(
&self,
timestamp: Timestamp,
) -> Result<Timestamp, Error> {
let increment =
Increment::for_timestamp(self.smallest, self.increment)?;
Timestamp::from_duration(
increment.round(self.mode, timestamp.as_duration())?,
)
}
}
impl Default for TimestampRound {
#[inline]
fn default() -> TimestampRound {
TimestampRound::new()
}
}
impl From<Unit> for TimestampRound {
#[inline]
fn from(unit: Unit) -> TimestampRound {
TimestampRound::default().smallest(unit)
}
}
impl From<(Unit, i64)> for TimestampRound {
#[inline]
fn from((unit, increment): (Unit, i64)) -> TimestampRound {
TimestampRound::from(unit).increment(increment)
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use std::io::Cursor;
use crate::{
civil::{self, datetime},
tz::Offset,
ToSpan,
};
use super::*;
fn mktime(seconds: i64, nanos: i32) -> Timestamp {
Timestamp::new(seconds, nanos).unwrap()
}
fn mkdt(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
nano: i32,
) -> civil::DateTime {
let date = civil::Date::new(year, month, day).unwrap();
let time = civil::Time::new(hour, minute, second, nano).unwrap();
civil::DateTime::from_parts(date, time)
}
#[test]
fn to_datetime_specific_examples() {
let tests = [
((b::UnixSeconds::MIN, 0), (-9999, 1, 2, 1, 59, 59, 0)),
(
(b::UnixSeconds::MIN + 1, -999_999_999),
(-9999, 1, 2, 1, 59, 59, 1),
),
((-1, 1), (1969, 12, 31, 23, 59, 59, 1)),
((b::UnixSeconds::MAX, 0), (9999, 12, 30, 22, 0, 0, 0)),
((b::UnixSeconds::MAX - 1, 0), (9999, 12, 30, 21, 59, 59, 0)),
(
(b::UnixSeconds::MAX - 1, 999_999_999),
(9999, 12, 30, 21, 59, 59, 999_999_999),
),
(
(b::UnixSeconds::MAX, 999_999_999),
(9999, 12, 30, 22, 0, 0, 999_999_999),
),
((-2, -1), (1969, 12, 31, 23, 59, 57, 999_999_999)),
((-86398, -1), (1969, 12, 31, 0, 0, 1, 999_999_999)),
((-86399, -1), (1969, 12, 31, 0, 0, 0, 999_999_999)),
((-86400, -1), (1969, 12, 30, 23, 59, 59, 999_999_999)),
];
for (t, dt) in tests {
let timestamp = mktime(t.0, t.1);
let datetime = mkdt(dt.0, dt.1, dt.2, dt.3, dt.4, dt.5, dt.6);
assert_eq!(
Offset::UTC.to_datetime(timestamp),
datetime,
"timestamp: {t:?}"
);
assert_eq!(
timestamp,
datetime.to_zoned(TimeZone::UTC).unwrap().timestamp(),
"datetime: {datetime:?}"
);
}
}
#[test]
fn to_datetime_many_seconds_in_some_days() {
let days = [
i64::from(b::UnixEpochDays::MIN),
-1000,
-5,
23,
2000,
i64::from(b::UnixEpochDays::MAX),
];
let seconds = [
-86_400, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 86_400,
];
let nanos = [0, 1, 5, 999_999_999];
for day in days {
let midpoint = day * 86_400;
for second in seconds {
let second = midpoint + second;
if b::UnixSeconds::check(second).is_err() {
continue;
}
for nano in nanos {
if second == b::UnixSeconds::MIN && nano != 0 {
continue;
}
let t = Timestamp::new(second, nano).unwrap();
let Ok(got) =
Offset::UTC.to_datetime(t).to_zoned(TimeZone::UTC)
else {
continue;
};
assert_eq!(t, got.timestamp());
}
}
}
}
#[test]
fn invalid_time() {
assert!(Timestamp::new(b::UnixSeconds::MIN, -1).is_err());
assert!(Timestamp::new(b::UnixSeconds::MIN, -999_999_999).is_err());
assert!(Timestamp::new(b::UnixSeconds::MIN, 1).is_ok());
assert!(Timestamp::new(b::UnixSeconds::MIN, 999_999_999).is_ok());
}
#[cfg(target_pointer_width = "64")]
#[test]
fn timestamp_size() {
#[cfg(debug_assertions)]
{
assert_eq!(16, core::mem::size_of::<Timestamp>());
}
#[cfg(not(debug_assertions))]
{
assert_eq!(16, core::mem::size_of::<Timestamp>());
}
}
#[test]
fn nanosecond_roundtrip_boundaries() {
let inst = Timestamp::MIN;
let nanos = inst.as_nanosecond();
assert_eq!(0, nanos % (b::NANOS_PER_SEC as i128));
let got = Timestamp::from_nanosecond(nanos).unwrap();
assert_eq!(inst, got);
let inst = Timestamp::MAX;
let nanos = inst.as_nanosecond();
assert_eq!(
b::SignedSubsecNanosecond::MAX as i128,
nanos % (b::NANOS_PER_SEC as i128)
);
let got = Timestamp::from_nanosecond(nanos).unwrap();
assert_eq!(inst, got);
}
#[test]
fn timestamp_saturating_add() {
insta::assert_snapshot!(
Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(),
@"operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
)
}
#[test]
fn timestamp_saturating_sub() {
insta::assert_snapshot!(
Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(),
@"operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
)
}
quickcheck::quickcheck! {
fn prop_unix_seconds_roundtrip(t: Timestamp) -> quickcheck::TestResult {
let dt = t.to_zoned(TimeZone::UTC).datetime();
let Ok(got) = dt.to_zoned(TimeZone::UTC) else {
return quickcheck::TestResult::discard();
};
quickcheck::TestResult::from_bool(t == got.timestamp())
}
fn prop_nanos_roundtrip_unix(t: Timestamp) -> bool {
let nanos = t.as_nanosecond();
let got = Timestamp::from_nanosecond(nanos).unwrap();
t == got
}
fn timestamp_constant_and_new_are_same1(t: Timestamp) -> bool {
let got = Timestamp::constant(t.as_second(), t.subsec_nanosecond());
t == got
}
fn timestamp_constant_and_new_are_same2(
secs: i64,
nanos: i32
) -> quickcheck::TestResult {
let Ok(ts) = Timestamp::new(secs, nanos) else {
return quickcheck::TestResult::discard();
};
let got = Timestamp::constant(secs, nanos);
quickcheck::TestResult::from_bool(ts == got)
}
}
#[test]
fn timestamp_deserialize_yaml() {
let expected = datetime(2024, 10, 31, 16, 33, 53, 123456789)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let deserialized: Timestamp =
serde_yaml::from_str("2024-10-31T16:33:53.123456789+00:00")
.unwrap();
assert_eq!(deserialized, expected);
let deserialized: Timestamp = serde_yaml::from_slice(
"2024-10-31T16:33:53.123456789+00:00".as_bytes(),
)
.unwrap();
assert_eq!(deserialized, expected);
let cursor = Cursor::new(b"2024-10-31T16:33:53.123456789+00:00");
let deserialized: Timestamp = serde_yaml::from_reader(cursor).unwrap();
assert_eq!(deserialized, expected);
}
#[test]
fn timestamp_precision_loss() {
let ts1: Timestamp =
"2025-01-25T19:32:21.783444592+01:00".parse().unwrap();
let span = 1.second();
let ts2 = ts1 + span;
assert_eq!(ts2.to_string(), "2025-01-25T18:32:22.783444592Z");
assert_eq!(ts1, ts2 - span, "should be reversible");
}
}