gstreamer-rs/gstreamer/src/date_time.rs
Bilal Elmoussaoui 4ebec84f5e Adapt to no longer renamed ffi crates
Allows us to set all the crates in the main workspace file, so changing
their versions or branch is much simpler and reduce the amount of noise
in the diff

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/merge_requests/1450>
2024-06-02 11:20:55 +02:00

743 lines
24 KiB
Rust

// Take a look at the license at the top of the repository in the LICENSE file.
use std::{cmp, fmt};
use glib::translate::*;
use crate::{ffi, DateTime};
// Validate that the given values result in a valid DateTime
fn validate(
tzoffset: Option<f32>,
year: i32,
month: Option<i32>,
day: Option<i32>,
hour: Option<i32>,
minute: Option<i32>,
seconds: Option<f64>,
) -> Result<(), glib::BoolError> {
skip_assert_initialized!();
// Check for valid ranges
if year <= 0 || year > 9999 {
return Err(glib::bool_error!(
"Can't create DateTime: Year out of range"
));
}
if let Some(month) = month {
if month <= 0 || month > 12 {
return Err(glib::bool_error!(
"Can't create DateTime: Month out of range"
));
}
}
if let Some(day) = day {
if day <= 0 || day > 31 {
return Err(glib::bool_error!("Can't create DateTime: Day out of range"));
}
}
if let Some(hour) = hour {
if hour < 0 || hour >= 24 {
return Err(glib::bool_error!(
"Can't create DateTime: Hour out of range"
));
}
}
if let Some(minute) = minute {
if minute < 0 || minute >= 60 {
return Err(glib::bool_error!(
"Can't create DateTime: Minute out of range"
));
}
}
if let Some(seconds) = seconds {
if seconds < 0.0 || seconds >= 60.0 {
return Err(glib::bool_error!(
"Can't create DateTime: Seconds out of range"
));
}
}
if let Some(tzoffset) = tzoffset {
if tzoffset < -12.0 || tzoffset > 12.0 {
return Err(glib::bool_error!(
"Can't create DateTime: Timezone offset out of range"
));
}
}
// If day is provided, month also has to be provided
if day.is_some() && month.is_none() {
return Err(glib::bool_error!(
"Can't create DateTime: Need to provide month if providing day"
));
}
// If hour is provided, day also has to be provided
if hour.is_some() && day.is_none() {
return Err(glib::bool_error!(
"Can't create DateTime: Need to provide day if providing hour"
));
}
// If minutes are provided, hours also need to be provided and the other way around
if hour.is_none() && minute.is_some() {
return Err(glib::bool_error!(
"Can't create DateTime: Need to provide both hour and minute or neither"
));
}
if minute.is_some() && hour.is_none() {
return Err(glib::bool_error!(
"Can't create DateTime: Need to provide both hour and minute or neither"
));
}
// If seconds or tzoffset are provided then also hours and minutes must be provided
if (seconds.is_some() || tzoffset.is_some()) && (hour.is_none() || minute.is_none()) {
return Err(glib::bool_error!("Can't create DateTime: Need to provide hour and minute if providing seconds or timezone offset"));
}
Ok(())
}
impl DateTime {
#[doc(alias = "gst_date_time_new")]
pub fn new(
tzoffset: impl Into<Option<f32>>,
year: impl Into<i32>,
month: impl Into<Option<i32>>,
day: impl Into<Option<i32>>,
hour: impl Into<Option<i32>>,
minute: impl Into<Option<i32>>,
seconds: impl Into<Option<f64>>,
) -> Result<DateTime, glib::BoolError> {
assert_initialized_main_thread!();
let tzoffset = tzoffset.into();
let year = year.into();
let month = month.into();
let day = day.into();
let hour = hour.into();
let minute = minute.into();
let seconds = seconds.into();
validate(tzoffset, year, month, day, hour, minute, seconds)?;
unsafe {
Option::<_>::from_glib_full(ffi::gst_date_time_new(
tzoffset.unwrap_or(0.0),
year,
month.unwrap_or(-1),
day.unwrap_or(-1),
hour.unwrap_or(-1),
minute.unwrap_or(-1),
seconds.unwrap_or(-1.0),
))
.ok_or_else(|| glib::bool_error!("Can't create DateTime"))
}
}
#[doc(alias = "gst_date_time_new_local_time")]
pub fn from_local_time(
year: impl Into<i32>,
month: impl Into<Option<i32>>,
day: impl Into<Option<i32>>,
hour: impl Into<Option<i32>>,
minute: impl Into<Option<i32>>,
seconds: impl Into<Option<f64>>,
) -> Result<DateTime, glib::BoolError> {
assert_initialized_main_thread!();
let year = year.into();
let month = month.into();
let day = day.into();
let hour = hour.into();
let minute = minute.into();
let seconds = seconds.into();
validate(None, year, month, day, hour, minute, seconds)?;
unsafe {
Option::<_>::from_glib_full(ffi::gst_date_time_new_local_time(
year,
month.unwrap_or(-1),
day.unwrap_or(-1),
hour.unwrap_or(-1),
minute.unwrap_or(-1),
seconds.unwrap_or(-1.0),
))
.ok_or_else(|| glib::bool_error!("Can't create DateTime"))
}
}
#[doc(alias = "gst_date_time_new_y")]
pub fn from_y(year: i32) -> Result<DateTime, glib::BoolError> {
assert_initialized_main_thread!();
validate(None, year, None, None, None, None, None)?;
unsafe {
Option::<_>::from_glib_full(ffi::gst_date_time_new_y(year))
.ok_or_else(|| glib::bool_error!("Can't create DateTime"))
}
}
#[doc(alias = "gst_date_time_new_ym")]
pub fn from_ym(year: i32, month: i32) -> Result<DateTime, glib::BoolError> {
assert_initialized_main_thread!();
validate(None, year, Some(month), None, None, None, None)?;
unsafe {
Option::<_>::from_glib_full(ffi::gst_date_time_new_ym(year, month))
.ok_or_else(|| glib::bool_error!("Can't create DateTime"))
}
}
#[doc(alias = "gst_date_time_new_ymd")]
pub fn from_ymd(year: i32, month: i32, day: i32) -> Result<DateTime, glib::BoolError> {
assert_initialized_main_thread!();
validate(None, year, Some(month), Some(day), None, None, None)?;
unsafe {
Option::<_>::from_glib_full(ffi::gst_date_time_new_ymd(year, month, day))
.ok_or_else(|| glib::bool_error!("Can't create DateTime"))
}
}
#[doc(alias = "get_day")]
#[doc(alias = "gst_date_time_get_day")]
pub fn day(&self) -> Option<i32> {
if !self.has_day() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_day(self.to_glib_none().0)) }
}
#[doc(alias = "get_hour")]
#[doc(alias = "gst_date_time_get_hour")]
pub fn hour(&self) -> Option<i32> {
if !self.has_time() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_hour(self.to_glib_none().0)) }
}
#[doc(alias = "get_microsecond")]
#[doc(alias = "gst_date_time_get_microsecond")]
pub fn microsecond(&self) -> Option<i32> {
if !self.has_second() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_microsecond(self.to_glib_none().0)) }
}
#[doc(alias = "get_minute")]
#[doc(alias = "gst_date_time_get_minute")]
pub fn minute(&self) -> Option<i32> {
if !self.has_time() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_minute(self.to_glib_none().0)) }
}
#[doc(alias = "get_month")]
#[doc(alias = "gst_date_time_get_month")]
pub fn month(&self) -> Option<i32> {
if !self.has_month() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_month(self.to_glib_none().0)) }
}
#[doc(alias = "get_second")]
#[doc(alias = "gst_date_time_get_second")]
pub fn second(&self) -> Option<i32> {
if !self.has_second() {
return None;
}
unsafe { Some(ffi::gst_date_time_get_second(self.to_glib_none().0)) }
}
#[doc(alias = "get_time_zone_offset")]
#[doc(alias = "gst_date_time_get_time_zone_offset")]
pub fn time_zone_offset(&self) -> Option<f32> {
if !self.has_time() {
return None;
}
unsafe {
Some(ffi::gst_date_time_get_time_zone_offset(
self.to_glib_none().0,
))
}
}
pub fn to_utc(&self) -> Result<DateTime, glib::BoolError> {
if !self.has_time() {
// No time => no TZ offset
return Ok(self.clone());
}
assert!(self.has_year() && self.has_month() && self.has_day() && self.has_time());
// Can instantiate `gst::DateTime` without seconds using `gst::DateTime::new`
// with `-1f64` for the `second` argument
// however, the resulting instance can't be translated to `glib::DateTime`
if self.has_second() {
self.to_g_date_time()
.and_then(|d| d.to_utc())
.map(|d| d.into())
} else {
// It would be cheaper to build a `glib::DateTime` directly, unfortunetaly
// this would require using `glib::TimeZone::new_offset` which is feature-gated
// to `glib/v2_58`. So we need to build a new `gst::DateTime` with `0f64`
// and then discard seconds again
DateTime::new(
self.time_zone_offset(),
self.year(),
self.month(),
self.day(),
self.hour(),
self.minute(),
Some(0.0),
)
.and_then(|d| d.to_g_date_time())
.and_then(|d| d.to_utc())
.and_then(|d| {
DateTime::new(
None, // UTC TZ offset
d.year(),
Some(d.month()),
Some(d.day_of_month()),
Some(d.hour()),
Some(d.minute()),
None, // No second
)
})
}
}
}
impl cmp::PartialOrd for DateTime {
// *NOTE 1:* When comparing a partially defined [`DateTime`](struct.DateTime.html) `d1`
// such as *"2019/8/20"* with a [`DateTime`](struct.DateTime.html) with a time part `d2`
// such as *"2019/8/20 21:10"*:
//
// - `d1` includes `d2`,
// - neither `d1` < `d2` nor `d1` > `d2`,
// - and `d1` != `d2`,
//
// so we can only return `None`.
//
// This is the reason why [`DateTime`](struct.DateTime.html) neither implements
// [`Ord`](https://doc.rust-lang.org/nightly/std/cmp/trait.Ord.html)
// nor [`Eq`](https://doc.rust-lang.org/nightly/std/cmp/trait.Eq.html).
//
// *NOTE 2:* When comparing a [`DateTime`](struct.DateTime.html) `d1` without a TZ offset
// such as *"2019/8/20"* with a [`DateTime`](struct.DateTime.html) `d2` with a TZ offset
// such as *"2019/8/20 21:10 +02:00"*, we can't tell in which TZ `d1` is expressed and which
// time should be considered for an offset, therefore the two [`DateTime`s](struct.DateTime.html)
// are compared in the same TZ.
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
#[inline]
#[allow(clippy::unnecessary_wraps)]
#[doc(alias = "get_cmp")]
fn cmp(delta: i32) -> Option<cmp::Ordering> {
skip_assert_initialized!();
Some(delta.cmp(&0))
}
if !(self.has_year() && other.has_year()) {
// Can't compare anything
return None;
}
// Normalize to UTC only if both members have time (see note 2).
let (self_norm, other_norm) = if self.has_time() && other.has_time() {
(self.to_utc().ok()?, other.to_utc().ok()?)
} else {
(self.clone(), other.clone())
};
let year_delta = self_norm.year() - other_norm.year();
if year_delta != 0 {
return cmp(year_delta);
}
// Same year
if !self.has_month() && !other.has_month() {
// Nothing left to compare
return cmp(year_delta);
}
if !(self.has_month() && other.has_month()) {
// One has month, the other doesn't => can't compare (note 1)
return None;
}
let month_delta = self_norm.month().unwrap() - other_norm.month().unwrap();
if month_delta != 0 {
return cmp(month_delta);
}
// Same year, same month
if !self.has_day() && !other.has_day() {
// Nothing left to compare
return Some(cmp::Ordering::Equal);
}
if !(self.has_day() && other.has_day()) {
// One has day, the other doesn't => can't compare (note 1)
return None;
}
let day_delta = self_norm.day().unwrap() - other_norm.day().unwrap();
if day_delta != 0 {
return cmp(day_delta);
}
// Same year, same month, same day
if !self.has_time() && !other.has_time() {
// Nothing left to compare
return Some(cmp::Ordering::Equal);
}
if !(self.has_time() && other.has_time()) {
// One has time, the other doesn't => can't compare (note 1)
return None;
}
let hour_delta = self_norm.hour().unwrap() - other_norm.hour().unwrap();
if hour_delta != 0 {
return cmp(hour_delta);
}
let minute_delta = self_norm.minute().unwrap() - other_norm.minute().unwrap();
if minute_delta != 0 {
return cmp(minute_delta);
}
// Same year, same month, same day, same time
if !self.has_second() && !other.has_second() {
// Nothing left to compare
return Some(cmp::Ordering::Equal);
}
if !(self.has_second() && other.has_second()) {
// One has second, the other doesn't => can't compare (note 1)
return None;
}
let second_delta = self_norm.second().unwrap() - other_norm.second().unwrap();
if second_delta != 0 {
return cmp(second_delta);
}
cmp(self_norm.microsecond().unwrap() - other_norm.microsecond().unwrap())
}
}
impl cmp::PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
self.partial_cmp(other)
.map_or_else(|| false, |cmp| cmp == cmp::Ordering::Equal)
}
}
impl fmt::Debug for DateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut debug_struct = f.debug_struct("DateTime");
if self.has_year() {
debug_struct.field("year", &self.year());
}
if self.has_month() {
debug_struct.field("month", &self.month());
}
if self.has_day() {
debug_struct.field("day", &self.day());
}
if self.has_time() {
debug_struct.field("hour", &self.hour());
debug_struct.field("minute", &self.minute());
if self.has_second() {
debug_struct.field("second", &self.second());
debug_struct.field("microsecond", &self.microsecond());
}
debug_struct.field("tz_offset", &self.time_zone_offset());
}
debug_struct.finish()
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(
self.to_iso8601_string()
.unwrap_or_else(|_| "None".into())
.as_str(),
)
}
}
impl<'a> From<&'a glib::DateTime> for DateTime {
fn from(v: &'a glib::DateTime) -> DateTime {
skip_assert_initialized!();
DateTime::from_g_date_time(v.clone())
}
}
impl From<glib::DateTime> for DateTime {
fn from(v: glib::DateTime) -> DateTime {
skip_assert_initialized!();
DateTime::from_g_date_time(v)
}
}
impl<'a> TryFrom<&'a DateTime> for glib::DateTime {
type Error = glib::BoolError;
fn try_from(v: &'a DateTime) -> Result<glib::DateTime, glib::BoolError> {
skip_assert_initialized!();
v.to_g_date_time()
}
}
impl TryFrom<DateTime> for glib::DateTime {
type Error = glib::BoolError;
fn try_from(v: DateTime) -> Result<glib::DateTime, glib::BoolError> {
skip_assert_initialized!();
v.to_g_date_time()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::cognitive_complexity)]
#[test]
fn test_to_utc() {
crate::init().unwrap();
// Hour offset
let utc_date_time = DateTime::new(2f32, 2019, 8, 20, 20, 9, 42.123_456f64)
.unwrap()
.to_utc()
.unwrap();
assert_eq!(utc_date_time.year(), 2019);
assert_eq!(utc_date_time.month().unwrap(), 8);
assert_eq!(utc_date_time.day().unwrap(), 20);
assert_eq!(utc_date_time.hour().unwrap(), 18);
assert_eq!(utc_date_time.minute().unwrap(), 9);
assert_eq!(utc_date_time.second().unwrap(), 42);
assert_eq!(utc_date_time.microsecond().unwrap(), 123_456);
// Year, month, day and hour offset
let utc_date_time = DateTime::new(2f32, 2019, 1, 1, 0, 0, 42.123_456f64)
.unwrap()
.to_utc()
.unwrap();
assert_eq!(utc_date_time.year(), 2018);
assert_eq!(utc_date_time.month().unwrap(), 12);
assert_eq!(utc_date_time.day().unwrap(), 31);
assert_eq!(utc_date_time.hour().unwrap(), 22);
assert_eq!(utc_date_time.minute().unwrap(), 0);
assert_eq!(utc_date_time.second().unwrap(), 42);
assert_eq!(utc_date_time.microsecond().unwrap(), 123_456);
// Date without an hour (which implies no TZ)
let utc_date_time = DateTime::from_ymd(2019, 1, 1).unwrap().to_utc().unwrap();
assert_eq!(utc_date_time.year(), 2019);
assert_eq!(utc_date_time.month().unwrap(), 1);
assert_eq!(utc_date_time.day().unwrap(), 1);
assert!(!utc_date_time.has_time());
assert!(!utc_date_time.has_second());
// Date without seconds
let utc_date_time = DateTime::new(2f32, 2018, 5, 28, 16, 6, None)
.unwrap()
.to_utc()
.unwrap();
assert_eq!(utc_date_time.year(), 2018);
assert_eq!(utc_date_time.month().unwrap(), 5);
assert_eq!(utc_date_time.day().unwrap(), 28);
assert_eq!(utc_date_time.hour().unwrap(), 14);
assert_eq!(utc_date_time.minute().unwrap(), 6);
assert!(!utc_date_time.has_second());
}
#[test]
fn test_partial_ord() {
crate::init().unwrap();
// Different years
assert!(
DateTime::new(2f32, 2020, 8, 20, 19, 43, 42.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different months (order intentionally reversed)
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
< DateTime::new(2f32, 2019, 9, 19, 19, 43, 42.123_456f64).unwrap()
);
// Different days
assert!(
DateTime::new(2f32, 2019, 8, 21, 19, 43, 42.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different hours
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 44, 42.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different minutes
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 43, 44.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different seconds
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 43, 43.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different micro-seconds
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_457f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// Different TZ offsets
assert!(
DateTime::new(1f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 42.123_456f64).unwrap()
);
// TZ offset leading to year, month, day, hour offset
assert!(
DateTime::new(2f32, 2019, 1, 1, 0, 0, 0f64).unwrap()
< DateTime::new(1f32, 2018, 12, 31, 23, 59, 0f64).unwrap()
);
// Partially defined `DateTime`
assert!(
DateTime::from_ymd(2020, 8, 20).unwrap() > DateTime::from_ymd(2019, 8, 20).unwrap()
);
assert!(
DateTime::from_ymd(2019, 9, 20).unwrap() > DateTime::from_ymd(2019, 8, 20).unwrap()
);
assert!(
DateTime::from_ymd(2019, 8, 21).unwrap() > DateTime::from_ymd(2019, 8, 20).unwrap()
);
assert!(DateTime::from_ym(2020, 8).unwrap() > DateTime::from_ym(2019, 8).unwrap());
assert!(DateTime::from_ym(2019, 9).unwrap() > DateTime::from_ym(2019, 8).unwrap());
assert!(DateTime::from_ym(2019, 9).unwrap() > DateTime::from_ymd(2019, 8, 20).unwrap());
assert!(DateTime::from_y(2020).unwrap() > DateTime::from_y(2019).unwrap());
assert!(DateTime::from_ym(2020, 1).unwrap() > DateTime::from_y(2019).unwrap());
assert!(
DateTime::new(2f32, 2019, 8, 20, 19, 43, 44.123_456f64).unwrap()
< DateTime::from_ymd(2020, 8, 20).unwrap()
);
assert!(
DateTime::from_ymd(2020, 8, 20).unwrap()
> DateTime::new(2f32, 2019, 8, 20, 19, 43, 44.123_456f64).unwrap()
);
// Comparison occurs on the same TZ when the `DateTime` doesn't have time (note 2)
assert!(
DateTime::from_ymd(2020, 1, 1).unwrap()
> DateTime::new(-2f32, 2019, 12, 31, 23, 59, 0f64).unwrap()
);
// In the following cases, the partially defined `DateTime` is a range WRT
// the fully defined `DateTime` and this range includes the fully defined `DateTime`,
// but we can't tell if it's before or after and they are not equal (note 1)
assert!(DateTime::new(2f32, 2019, 8, 20, 19, 43, 44.123_456f64)
.unwrap()
.partial_cmp(&DateTime::from_ymd(2019, 8, 20).unwrap())
.is_none());
assert!(DateTime::from_ymd(2019, 8, 20)
.unwrap()
.partial_cmp(&DateTime::new(2f32, 2019, 8, 20, 19, 43, 44.123_456f64).unwrap())
.is_none());
assert!(DateTime::from_ym(2019, 1)
.unwrap()
.partial_cmp(&DateTime::from_y(2019).unwrap())
.is_none());
}
#[test]
fn test_eq() {
crate::init().unwrap();
assert_eq!(
DateTime::new(2f32, 2018, 5, 28, 16, 6, 42.123_456f64).unwrap(),
DateTime::new(2f32, 2018, 5, 28, 16, 6, 42.123_456f64).unwrap()
);
assert_eq!(
DateTime::new(2f32, 2018, 5, 28, 16, 6, 0f64).unwrap(),
DateTime::new(2f32, 2018, 5, 28, 16, 6, 0f64).unwrap()
);
assert_eq!(
DateTime::new(2f32, 2018, 5, 28, 16, 6, None).unwrap(),
DateTime::new(2f32, 2018, 5, 28, 16, 6, None).unwrap()
);
assert_eq!(
DateTime::from_ymd(2018, 5, 28).unwrap(),
DateTime::from_ymd(2018, 5, 28).unwrap()
);
// In the following cases, the partially defined `DateTime` is a range WRT
// the fully defined `DateTime` and this range includes the fully defined `DateTime`,
// but they are not equal (note 1)
assert_ne!(
DateTime::from_ymd(2018, 5, 28).unwrap(),
DateTime::new(2f32, 2018, 5, 28, 16, 6, None).unwrap()
);
assert_ne!(
DateTime::new(2f32, 2018, 5, 28, 16, 6, None).unwrap(),
DateTime::from_ym(2018, 5).unwrap()
);
assert_ne!(
DateTime::new(2f32, 2018, 5, 28, 16, 6, None).unwrap(),
DateTime::from_y(2018).unwrap()
);
}
}