use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::ops::{Mul, Neg};
#[cfg(feature = "timezones")]
use arrow::legacy::kernels::{Ambiguous, NonExistent};
use arrow::legacy::time_zone::Tz;
use arrow::temporal_conversions::{
timestamp_ms_to_datetime, timestamp_ns_to_datetime, timestamp_us_to_datetime, MILLISECONDS,
NANOSECONDS,
};
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use polars_core::datatypes::DataType;
use polars_core::export::arrow::temporal_conversions::MICROSECONDS;
use polars_core::prelude::{
datetime_to_timestamp_ms, datetime_to_timestamp_ns, datetime_to_timestamp_us, polars_bail,
PolarsResult,
};
use polars_error::polars_ensure;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::calendar::{
NS_DAY, NS_HOUR, NS_MICROSECOND, NS_MILLISECOND, NS_MINUTE, NS_SECOND, NS_WEEK,
};
#[cfg(feature = "timezones")]
use crate::utils::{localize_datetime_opt, try_localize_datetime, unlocalize_datetime};
use crate::windows::calendar::{is_leap_year, DAYS_PER_MONTH};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Duration {
months: i64,
weeks: i64,
days: i64,
nsecs: i64,
pub(crate) negative: bool,
pub parsed_int: bool,
}
impl PartialOrd<Self> for Duration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Duration {
fn cmp(&self, other: &Self) -> Ordering {
self.duration_ns().cmp(&other.duration_ns())
}
}
impl Neg for Duration {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
months: self.months,
weeks: self.weeks,
days: self.days,
nsecs: self.nsecs,
negative: !self.negative,
parsed_int: self.parsed_int,
}
}
}
impl Display for Duration {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_zero() {
return write!(f, "0s");
}
if self.negative {
write!(f, "-")?
}
if self.months > 0 {
write!(f, "{}m", self.months)?
}
if self.weeks > 0 {
write!(f, "{}w", self.weeks)?
}
if self.days > 0 {
write!(f, "{}d", self.days)?
}
if self.nsecs > 0 {
let secs = self.nsecs / NANOSECONDS;
if secs * NANOSECONDS == self.nsecs {
write!(f, "{}s", secs)?
} else {
let us = self.nsecs / 1_000;
if us * 1_000 == self.nsecs {
write!(f, "{}us", us)?
} else {
write!(f, "{}ns", self.nsecs)?
}
}
}
Ok(())
}
}
impl Duration {
pub fn new(fixed_slots: i64) -> Self {
Duration {
months: 0,
weeks: 0,
days: 0,
nsecs: fixed_slots.abs(),
negative: fixed_slots < 0,
parsed_int: true,
}
}
pub fn parse(duration: &str) -> Self {
Self::try_parse(duration).unwrap()
}
#[doc(hidden)]
pub fn parse_interval(interval: &str) -> Self {
Self::try_parse_interval(interval).unwrap()
}
pub fn try_parse(duration: &str) -> PolarsResult<Self> {
Self::_parse(duration, false)
}
pub fn try_parse_interval(interval: &str) -> PolarsResult<Self> {
Self::_parse(&interval.to_ascii_lowercase(), true)
}
fn _parse(s: &str, as_interval: bool) -> PolarsResult<Self> {
let s = if as_interval { s.trim_start() } else { s };
let parse_type = if as_interval { "interval" } else { "duration" };
let num_minus_signs = s.matches('-').count();
if num_minus_signs > 1 {
polars_bail!(InvalidOperation: "{} string can only have a single minus sign", parse_type);
}
if num_minus_signs > 0 {
if as_interval {
polars_bail!(InvalidOperation: "minus signs are not currently supported in interval strings");
} else if !s.starts_with('-') {
polars_bail!(InvalidOperation: "only a single minus sign is allowed, at the front of the string");
}
}
let mut months = 0;
let mut weeks = 0;
let mut days = 0;
let mut nsecs = 0;
let negative = s.starts_with('-');
let mut iter = s.char_indices().peekable();
let mut start = 0;
if negative {
start += 1;
iter.next().unwrap();
}
if as_interval {
while let Some((i, ch)) = iter.peek() {
if *ch == ' ' {
start = *i + 1;
iter.next();
} else {
break;
}
}
}
let mut unit = String::with_capacity(12);
let mut parsed_int = false;
while let Some((i, mut ch)) = iter.next() {
if !ch.is_ascii_digit() {
let Ok(n) = s[start..i].parse::<i64>() else {
polars_bail!(InvalidOperation:
"expected leading integer in the {} string, found {}",
parse_type, ch
);
};
loop {
match ch {
c if c.is_ascii_alphabetic() => unit.push(c),
' ' | ',' if as_interval => {},
_ => break,
}
match iter.next() {
Some((i, ch_)) => {
ch = ch_;
start = i
},
None => break,
}
}
if unit.is_empty() {
polars_bail!(InvalidOperation:
"expected a unit to follow integer in the {} string '{}'",
parse_type, s
);
}
match &*unit {
"ns" => nsecs += n,
"us" => nsecs += n * NS_MICROSECOND,
"ms" => nsecs += n * NS_MILLISECOND,
"s" => nsecs += n * NS_SECOND,
"m" => nsecs += n * NS_MINUTE,
"h" => nsecs += n * NS_HOUR,
"d" => days += n,
"w" => weeks += n,
"mo" => months += n,
"q" => months += n * 3,
"y" => months += n * 12,
"i" => {
nsecs += n;
parsed_int = true;
},
_ if as_interval => match &*unit {
"nanosecond" | "nanoseconds" => nsecs += n,
"microsecond" | "microseconds" => nsecs += n * NS_MICROSECOND,
"millisecond" | "milliseconds" => nsecs += n * NS_MILLISECOND,
"sec" | "secs" | "second" | "seconds" => nsecs += n * NS_SECOND,
"min" | "mins" | "minute" | "minutes" => nsecs += n * NS_MINUTE,
"hour" | "hours" => nsecs += n * NS_HOUR,
"day" | "days" => days += n,
"week" | "weeks" => weeks += n,
"mon" | "mons" | "month" | "months" => months += n,
"quarter" | "quarters" => months += n * 3,
"year" | "years" => months += n * 12,
_ => {
let valid_units = "'year', 'month', 'quarter', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'";
polars_bail!(InvalidOperation: "unit: '{unit}' not supported; available units include: {} (and their plurals)", valid_units);
},
},
_ => {
polars_bail!(InvalidOperation: "unit: '{unit}' not supported; available units are: 'y', 'mo', 'q', 'w', 'd', 'h', 'm', 's', 'ms', 'us', 'ns'");
},
}
unit.clear();
}
}
Ok(Duration {
nsecs: nsecs.abs(),
days: days.abs(),
weeks: weeks.abs(),
months: months.abs(),
negative,
parsed_int,
})
}
fn to_positive(v: i64) -> (bool, i64) {
if v < 0 {
(true, -v)
} else {
(false, v)
}
}
#[allow(dead_code)]
pub(crate) fn normalize(&self, interval: &Duration) -> Self {
if self.months_only() && interval.months_only() {
let mut months = self.months() % interval.months();
match (self.negative, interval.negative) {
(true, true) | (true, false) => months = -months + interval.months(),
_ => {},
}
Duration::from_months(months)
} else if self.weeks_only() && interval.weeks_only() {
let mut weeks = self.weeks() % interval.weeks();
match (self.negative, interval.negative) {
(true, true) | (true, false) => weeks = -weeks + interval.weeks(),
_ => {},
}
Duration::from_weeks(weeks)
} else if self.days_only() && interval.days_only() {
let mut days = self.days() % interval.days();
match (self.negative, interval.negative) {
(true, true) | (true, false) => days = -days + interval.days(),
_ => {},
}
Duration::from_days(days)
} else {
let mut offset = self.duration_ns();
if offset == 0 {
return *self;
}
let every = interval.duration_ns();
if offset < 0 {
offset += every * ((offset / -every) + 1)
} else {
offset -= every * (offset / every)
}
Duration::from_nsecs(offset)
}
}
pub(crate) fn from_nsecs(v: i64) -> Self {
let (negative, nsecs) = Self::to_positive(v);
Self {
months: 0,
weeks: 0,
days: 0,
nsecs,
negative,
parsed_int: false,
}
}
pub(crate) fn from_months(v: i64) -> Self {
let (negative, months) = Self::to_positive(v);
Self {
months,
weeks: 0,
days: 0,
nsecs: 0,
negative,
parsed_int: false,
}
}
pub(crate) fn from_weeks(v: i64) -> Self {
let (negative, weeks) = Self::to_positive(v);
Self {
months: 0,
weeks,
days: 0,
nsecs: 0,
negative,
parsed_int: false,
}
}
pub(crate) fn from_days(v: i64) -> Self {
let (negative, days) = Self::to_positive(v);
Self {
months: 0,
weeks: 0,
days,
nsecs: 0,
negative,
parsed_int: false,
}
}
pub fn is_zero(&self) -> bool {
self.months == 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
}
pub fn months_only(&self) -> bool {
self.months != 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
}
pub fn months(&self) -> i64 {
self.months
}
pub fn weeks_only(&self) -> bool {
self.months == 0 && self.weeks != 0 && self.days == 0 && self.nsecs == 0
}
pub fn weeks(&self) -> i64 {
self.weeks
}
pub fn days_only(&self) -> bool {
self.months == 0 && self.weeks == 0 && self.days != 0 && self.nsecs == 0
}
pub fn days(&self) -> i64 {
self.days
}
pub fn is_full_days(&self) -> bool {
self.nsecs == 0
}
pub fn is_constant_duration(&self, time_zone: Option<&str>) -> bool {
if time_zone.is_none() || time_zone == Some("UTC") {
self.months == 0
} else {
self.months == 0 && self.weeks == 0 && self.days == 0
}
}
pub fn nanoseconds(&self) -> i64 {
self.nsecs
}
pub fn negative(&self) -> bool {
self.negative
}
#[doc(hidden)]
pub const fn duration_ns(&self) -> i64 {
self.months * 28 * 24 * 3600 * NANOSECONDS
+ self.weeks * NS_WEEK
+ self.days * NS_DAY
+ self.nsecs
}
#[doc(hidden)]
pub const fn duration_us(&self) -> i64 {
self.months * 28 * 24 * 3600 * MICROSECONDS
+ (self.weeks * NS_WEEK / 1000 + self.nsecs / 1000 + self.days * NS_DAY / 1000)
}
#[doc(hidden)]
pub const fn duration_ms(&self) -> i64 {
self.months * 28 * 24 * 3600 * MILLISECONDS
+ (self.weeks * NS_WEEK / 1_000_000
+ self.nsecs / 1_000_000
+ self.days * NS_DAY / 1_000_000)
}
#[doc(hidden)]
fn add_month(ts: NaiveDateTime, n_months: i64, negative: bool) -> NaiveDateTime {
let mut months = n_months;
if negative {
months = -months;
}
let mut year = ts.year();
let mut month = ts.month() as i32;
let mut day = ts.day();
year += (months / 12) as i32;
month += (months % 12) as i32;
if month > 12 {
year += 1;
month -= 12;
} else if month <= 0 {
year -= 1;
month += 12;
}
let last_day_of_month =
DAYS_PER_MONTH[is_leap_year(year) as usize][(month - 1) as usize] as u32;
if day > last_day_of_month {
day = last_day_of_month
}
let hour = ts.hour();
let minute = ts.minute();
let sec = ts.second();
let nsec = ts.nanosecond();
new_datetime(year, month as u32, day, hour, minute, sec, nsec).expect(
"Expected valid datetime, please open an issue at https://github.com/pola-rs/polars/issues"
)
}
#[cfg(feature = "timezones")]
fn localize_result(
&self,
original_dt_local: NaiveDateTime,
original_dt_utc: NaiveDateTime,
result_dt_local: NaiveDateTime,
tz: &Tz,
) -> PolarsResult<NaiveDateTime> {
match localize_datetime_opt(result_dt_local, tz, Ambiguous::Raise) {
Some(dt) => Ok(dt.expect("we didn't use Ambiguous::Null")),
None => {
if try_localize_datetime(
original_dt_local,
tz,
Ambiguous::Earliest,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null")
== original_dt_utc
{
Ok(try_localize_datetime(
result_dt_local,
tz,
Ambiguous::Earliest,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null"))
} else if try_localize_datetime(
original_dt_local,
tz,
Ambiguous::Latest,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null")
== original_dt_utc
{
Ok(try_localize_datetime(
result_dt_local,
tz,
Ambiguous::Latest,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null"))
} else {
unreachable!()
}
},
}
}
fn truncate_subweekly<G, J>(
&self,
t: i64,
tz: Option<&Tz>,
duration: i64,
_timestamp_to_datetime: G,
_datetime_to_timestamp: J,
) -> PolarsResult<i64>
where
G: Fn(i64) -> NaiveDateTime,
J: Fn(NaiveDateTime) -> i64,
{
match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
let original_dt_utc = _timestamp_to_datetime(t);
let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
let t = _datetime_to_timestamp(original_dt_local);
let mut remainder = t % duration;
if remainder < 0 {
remainder += duration
}
let result_timestamp = t - remainder;
let result_dt_local = _timestamp_to_datetime(result_timestamp);
let result_dt_utc =
self.localize_result(original_dt_local, original_dt_utc, result_dt_local, tz)?;
Ok(_datetime_to_timestamp(result_dt_utc))
},
_ => {
let mut remainder = t % duration;
if remainder < 0 {
remainder += duration
}
Ok(t - remainder)
},
}
}
fn truncate_weekly<G, J>(
&self,
t: i64,
tz: Option<&Tz>,
_timestamp_to_datetime: G,
_datetime_to_timestamp: J,
daily_duration: i64,
) -> PolarsResult<i64>
where
G: Fn(i64) -> NaiveDateTime,
J: Fn(NaiveDateTime) -> i64,
{
let _original_dt_utc: Option<NaiveDateTime>;
let _original_dt_local: Option<NaiveDateTime>;
let t = match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
_original_dt_utc = Some(_timestamp_to_datetime(t));
_original_dt_local = Some(unlocalize_datetime(_original_dt_utc.unwrap(), tz));
_datetime_to_timestamp(_original_dt_local.unwrap())
},
_ => {
_original_dt_utc = None;
_original_dt_local = None;
t
},
};
let mut remainder = (t - 4 * daily_duration) % (7 * self.weeks * daily_duration);
if remainder < 0 {
remainder += 7 * self.weeks * daily_duration
}
let result_t_local = t - remainder;
match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
let result_dt_local = _timestamp_to_datetime(result_t_local);
let result_dt_utc = self.localize_result(
_original_dt_local.unwrap(),
_original_dt_utc.unwrap(),
result_dt_local,
tz,
)?;
Ok(_datetime_to_timestamp(result_dt_utc))
},
_ => Ok(result_t_local),
}
}
fn truncate_monthly<G, J>(
&self,
t: i64,
tz: Option<&Tz>,
timestamp_to_datetime: G,
datetime_to_timestamp: J,
daily_duration: i64,
) -> PolarsResult<i64>
where
G: Fn(i64) -> NaiveDateTime,
J: Fn(NaiveDateTime) -> i64,
{
let original_dt_utc;
let original_dt_local;
let t = match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
original_dt_utc = timestamp_to_datetime(t);
original_dt_local = unlocalize_datetime(original_dt_utc, tz);
datetime_to_timestamp(original_dt_local)
},
_ => {
original_dt_utc = timestamp_to_datetime(t);
original_dt_local = original_dt_utc;
datetime_to_timestamp(original_dt_local)
},
};
let mut remainder_time = t % daily_duration;
if remainder_time < 0 {
remainder_time += daily_duration
}
let t = t - remainder_time;
let (mut year, mut month) = (
original_dt_local.year() as i64,
original_dt_local.month() as i64,
);
let total = (year * 12) + (month - 1);
let mut remainder_months = total % self.months;
if remainder_months < 0 {
remainder_months += self.months
}
let mut _is_leap_year = is_leap_year(year as i32);
let mut remainder_days = (original_dt_local.day() - 1) as i64;
while remainder_months > 12 {
let prev_year_is_leap_year = is_leap_year((year - 1) as i32);
let add_extra_day =
(_is_leap_year && month > 2) || (prev_year_is_leap_year && month <= 2);
remainder_days += 365 + add_extra_day as i64;
remainder_months -= 12;
year -= 1;
_is_leap_year = prev_year_is_leap_year;
}
while remainder_months > 0 {
month -= 1;
if month == 0 {
year -= 1;
_is_leap_year = is_leap_year(year as i32);
month = 12;
}
remainder_days += DAYS_PER_MONTH[_is_leap_year as usize][(month - 1) as usize];
remainder_months -= 1;
}
match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
let result_dt_local = timestamp_to_datetime(t - remainder_days * daily_duration);
let result_dt_utc =
self.localize_result(original_dt_local, original_dt_utc, result_dt_local, tz)?;
Ok(datetime_to_timestamp(result_dt_utc))
},
_ => Ok(t - remainder_days * daily_duration),
}
}
#[inline]
pub fn truncate_impl<F, G, J>(
&self,
t: i64,
tz: Option<&Tz>,
nsecs_to_unit: F,
timestamp_to_datetime: G,
datetime_to_timestamp: J,
) -> PolarsResult<i64>
where
F: Fn(i64) -> i64,
G: Fn(i64) -> NaiveDateTime,
J: Fn(NaiveDateTime) -> i64,
{
match (self.months, self.weeks, self.days, self.nsecs) {
(0, 0, 0, 0) => polars_bail!(ComputeError: "duration cannot be zero"),
(0, 0, 0, _) => {
let duration = nsecs_to_unit(self.nsecs);
self.truncate_subweekly(
t,
tz,
duration,
timestamp_to_datetime,
datetime_to_timestamp,
)
},
(0, 0, _, 0) => {
let duration = self.days * nsecs_to_unit(NS_DAY);
self.truncate_subweekly(
t,
tz,
duration,
timestamp_to_datetime,
datetime_to_timestamp,
)
},
(0, _, 0, 0) => {
let duration = nsecs_to_unit(NS_DAY);
self.truncate_weekly(
t,
tz,
timestamp_to_datetime,
datetime_to_timestamp,
duration,
)
},
(_, 0, 0, 0) => {
let duration = nsecs_to_unit(NS_DAY);
self.truncate_monthly(
t,
tz,
timestamp_to_datetime,
datetime_to_timestamp,
duration,
)
},
_ => {
polars_bail!(ComputeError: "duration may not mix month, weeks and nanosecond units")
},
}
}
#[inline]
pub fn truncate_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
self.truncate_impl(
t,
tz,
|nsecs| nsecs,
timestamp_ns_to_datetime,
datetime_to_timestamp_ns,
)
}
#[inline]
pub fn truncate_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
self.truncate_impl(
t,
tz,
|nsecs| nsecs / 1000,
timestamp_us_to_datetime,
datetime_to_timestamp_us,
)
}
#[inline]
pub fn truncate_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
self.truncate_impl(
t,
tz,
|nsecs| nsecs / 1_000_000,
timestamp_ms_to_datetime,
datetime_to_timestamp_ms,
)
}
fn add_impl_month_week_or_day<F, G, J>(
&self,
t: i64,
tz: Option<&Tz>,
nsecs_to_unit: F,
timestamp_to_datetime: G,
datetime_to_timestamp: J,
) -> PolarsResult<i64>
where
F: Fn(i64) -> i64,
G: Fn(i64) -> NaiveDateTime,
J: Fn(NaiveDateTime) -> i64,
{
let d = self;
let mut new_t = t;
if d.months > 0 {
let ts = match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
unlocalize_datetime(timestamp_to_datetime(t), tz)
},
_ => timestamp_to_datetime(t),
};
let dt = Self::add_month(ts, d.months, d.negative);
new_t = match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => datetime_to_timestamp(
try_localize_datetime(dt, tz, Ambiguous::Raise, NonExistent::Raise)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null"),
),
_ => datetime_to_timestamp(dt),
};
}
if d.weeks > 0 {
let t_weeks = nsecs_to_unit(NS_WEEK) * self.weeks;
match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
new_t =
datetime_to_timestamp(unlocalize_datetime(timestamp_to_datetime(t), tz));
new_t += if d.negative { -t_weeks } else { t_weeks };
new_t = datetime_to_timestamp(
try_localize_datetime(
timestamp_to_datetime(new_t),
tz,
Ambiguous::Raise,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null"),
);
},
_ => new_t += if d.negative { -t_weeks } else { t_weeks },
};
}
if d.days > 0 {
let t_days = nsecs_to_unit(NS_DAY) * self.days;
match tz {
#[cfg(feature = "timezones")]
Some(tz) if tz != &chrono_tz::UTC => {
new_t =
datetime_to_timestamp(unlocalize_datetime(timestamp_to_datetime(t), tz));
new_t += if d.negative { -t_days } else { t_days };
new_t = datetime_to_timestamp(
try_localize_datetime(
timestamp_to_datetime(new_t),
tz,
Ambiguous::Raise,
NonExistent::Raise,
)?
.expect("we didn't use Ambiguous::Null or NonExistent::Null"),
);
},
_ => new_t += if d.negative { -t_days } else { t_days },
};
}
Ok(new_t)
}
pub fn add_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
let d = self;
let new_t = self.add_impl_month_week_or_day(
t,
tz,
|nsecs| nsecs,
timestamp_ns_to_datetime,
datetime_to_timestamp_ns,
);
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
Ok(new_t? + nsecs)
}
pub fn add_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
let d = self;
let new_t = self.add_impl_month_week_or_day(
t,
tz,
|nsecs| nsecs / 1000,
timestamp_us_to_datetime,
datetime_to_timestamp_us,
);
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
Ok(new_t? + nsecs / 1_000)
}
pub fn add_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
let d = self;
let new_t = self.add_impl_month_week_or_day(
t,
tz,
|nsecs| nsecs / 1_000_000,
timestamp_ms_to_datetime,
datetime_to_timestamp_ms,
);
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
Ok(new_t? + nsecs / 1_000_000)
}
}
impl Mul<i64> for Duration {
type Output = Self;
fn mul(mut self, mut rhs: i64) -> Self {
if rhs < 0 {
rhs = -rhs;
self.negative = !self.negative
}
self.months *= rhs;
self.weeks *= rhs;
self.days *= rhs;
self.nsecs *= rhs;
self
}
}
fn new_datetime(
year: i32,
month: u32,
days: u32,
hour: u32,
min: u32,
sec: u32,
nano: u32,
) -> Option<NaiveDateTime> {
let date = NaiveDate::from_ymd_opt(year, month, days)?;
let time = NaiveTime::from_hms_nano_opt(hour, min, sec, nano)?;
Some(NaiveDateTime::new(date, time))
}
pub fn ensure_is_constant_duration(
duration: Duration,
time_zone: Option<&str>,
variable_name: &str,
) -> PolarsResult<()> {
polars_ensure!(duration.is_constant_duration(time_zone),
InvalidOperation: "expected `{}` to be a constant duration \
(i.e. one independent of differing month durations or of daylight savings time), got {}.\n\
\n\
You may want to try:\n\
- using `'730h'` instead of `'1mo'`\n\
- using `'24h'` instead of `'1d'` if your series is time-zone-aware", variable_name, duration);
Ok(())
}
pub fn ensure_duration_matches_dtype(
duration: Duration,
dtype: &DataType,
variable_name: &str,
) -> PolarsResult<()> {
match dtype {
DataType::Int64 | DataType::UInt64 | DataType::Int32 | DataType::UInt32 => {
polars_ensure!(duration.parsed_int || duration.is_zero(),
InvalidOperation: "`{}` duration must be a parsed integer (i.e. use '2i', not '2d') when working with a numeric column", variable_name);
},
DataType::Datetime(_, _) | DataType::Date | DataType::Duration(_) | DataType::Time => {
polars_ensure!(!duration.parsed_int,
InvalidOperation: "`{}` duration may not be a parsed integer (i.e. use '2d', not '2i') when working with a temporal column", variable_name);
},
_ => {
polars_bail!(InvalidOperation: "unsupported data type: {} for `{}`, expected UInt64, UInt32, Int64, Int32, Datetime, Date, Duration, or Time", dtype, variable_name)
},
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse() {
let out = Duration::parse("1ns");
assert_eq!(out.nsecs, 1);
let out = Duration::parse("1ns1ms");
assert_eq!(out.nsecs, NS_MILLISECOND + 1);
let out = Duration::parse("123ns40ms");
assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
let out = Duration::parse("123ns40ms1w");
assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
assert_eq!(out.duration_ns(), 40 * NS_MILLISECOND + 123 + NS_WEEK);
let out = Duration::parse("-123ns40ms1w");
assert!(out.negative);
let out = Duration::parse("5w");
assert_eq!(out.weeks(), 5);
}
#[test]
fn test_add_ns() {
let t = 1;
let seven_days = Duration::parse("7d");
let one_week = Duration::parse("1w");
assert_eq!(
seven_days.add_ns(t, None).unwrap(),
one_week.add_ns(t, None).unwrap()
);
let seven_days_negative = Duration::parse("-7d");
let one_week_negative = Duration::parse("-1w");
assert_eq!(
seven_days_negative.add_ns(t, None).unwrap(),
one_week_negative.add_ns(t, None).unwrap()
);
}
#[test]
fn test_display() {
let duration = Duration::parse("1h");
let expected = "3600s";
assert_eq!(format!("{duration}"), expected);
let duration = Duration::parse("1h5ns");
let expected = "3600000000005ns";
assert_eq!(format!("{duration}"), expected);
let duration = Duration::parse("1h5000ns");
let expected = "3600000005us";
assert_eq!(format!("{duration}"), expected);
}
}