Browse Source

Using our own types instead of moving chrono all around

master
Julio Biason 5 years ago
parent
commit
047e2869ca
  1. 1
      Cargo.lock
  2. 6
      Cargo.toml
  3. 9
      src/args.rs
  4. 151
      src/date.rs
  5. 29
      src/date_errors.rs
  6. 151
      src/datetime.rs
  7. 40
      src/eventlist/event/eventtype.rs
  8. 98
      src/eventlist/event/mod.rs
  9. 9
      src/eventlist/eventlist.rs
  10. 36
      src/main.rs

1
Cargo.lock generated

@ -55,6 +55,7 @@ checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
dependencies = [
"num-integer",
"num-traits",
"serde",
"time",
]

6
Cargo.toml

@ -6,11 +6,11 @@ authors = ["Julio Biason <julio.biason@gmail.com>"]
edition = "2018"
[dependencies]
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
clap = "2.33"
env_logger = "0.7"
log = "*"
serde = "*"
serde_derive = "*"
toml = "0.5"
uuid = { version = "0.8", features = ["v4"] }
log = "*"
env_logger = "0.7"

9
src/args.rs

@ -25,7 +25,9 @@ use clap::Arg;
use clap::ArgMatches;
use clap::SubCommand;
use crate::date::{Date, DateError};
use crate::date::Date;
use crate::date_errors::DateError;
use crate::datetime::DateTime;
type Description = String;
@ -44,7 +46,7 @@ impl From<DateError> for ParseError {
pub enum Action {
List,
Add(Description, Date),
AddWithTime(Description, String, String),
AddWithTime(Description, DateTime),
}
pub fn parse() -> Result<Action, ParseError> {
@ -92,8 +94,7 @@ fn parse_add(arguments: &ArgMatches) -> Result<Action, ParseError> {
if let Some(time) = arguments.value_of("time") {
Ok(Action::AddWithTime(
description.into(),
date.into(),
time.into(),
DateTime::try_from(date, time)?,
))
} else {
Ok(Action::Add(description.into(), Date::try_from(date)?))

151
src/date.rs

@ -16,31 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![deny(missing_docs)]
// TODO trait TryFrom
use chrono::prelude::*;
use chrono::LocalResult;
use serde_derive::Deserialize;
use serde_derive::Serialize;
#[derive(Debug, Eq, PartialEq)]
pub enum DateError {
/// The date is not valid
InvalidDate,
}
impl From<std::num::ParseIntError> for DateError {
fn from(_: std::num::ParseIntError) -> DateError {
DateError::InvalidDate
}
}
use crate::date_errors::DateError;
#[derive(Debug)]
pub struct Date(chrono::Date<Local>);
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct Date(chrono::NaiveDate);
impl Date {
/// Returns Ok with the Date or Error in an invalid Date.
pub fn new(year: u16, month: u8, day: u8) -> Result<Date, DateError> {
match Local.ymd_opt(year as i32, month as u32, day as u32) {
LocalResult::Single(x) => Ok(Date(x)),
LocalResult::Single(x) => Ok(Date(x.naive_local())),
LocalResult::None => Err(DateError::InvalidDate),
LocalResult::Ambiguous(_, _) => Err(DateError::InvalidDate),
}
@ -70,84 +64,85 @@ impl Date {
/// Number of days till the date; None if the date is in the past.
pub fn eta(&self) -> Option<u16> {
let days = (self.0 - Local::today()).num_days();
let days = (self.0 - Local::today().naive_local()).num_days();
if days >= 0 {
Some(days as u16)
} else {
None
}
}
}
#[cfg(test)]
#[test]
pub fn invalid_date() {
assert!(Date::new(2020, 127, 26).is_err());
assert!(Date::new(2020, 5, 127).is_err());
assert!(Date::new(2020, 0, 0).is_err());
pub fn timestamp(&self) -> i64 {
self.0.and_hms(23, 59, 59).timestamp()
}
}
#[cfg(test)]
#[test]
pub fn valid_date() {
assert!(Date::new(2025, 5, 26).is_ok());
}
mod date_test {
use chrono::prelude::*;
use chrono::Duration;
#[cfg(test)]
#[test]
pub fn from_string() {
if let Ok(dt) = Date::try_from("2025-05-26") {
assert_eq!(dt.year(), 2025);
assert_eq!(dt.month(), 5);
assert_eq!(dt.day(), 26);
} else {
panic!("Can't parse 2025-05-26")
#[test]
pub fn invalid_date() {
assert!(super::Date::new(2020, 127, 26).is_err());
assert!(super::Date::new(2020, 5, 127).is_err());
assert!(super::Date::new(2020, 0, 0).is_err());
}
}
#[cfg(test)]
#[test]
pub fn failed_from_string() {
assert!(Date::try_from("2020-127-26").is_err());
}
#[test]
pub fn valid_date() {
assert!(super::Date::new(2025, 5, 26).is_ok());
}
#[cfg(test)]
#[test]
pub fn eta_tomorrow() {
use chrono::Duration;
let future = Local::today() + Duration::days(1);
let date = Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), Some(1));
}
#[test]
pub fn from_string() {
if let Ok(dt) = super::Date::try_from("2025-05-26") {
assert_eq!(dt.year(), 2025);
assert_eq!(dt.month(), 5);
assert_eq!(dt.day(), 26);
} else {
panic!("Can't parse 2025-05-26")
}
}
#[cfg(test)]
#[test]
pub fn eta_today() {
let future = Local::today();
let date = Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), Some(0));
}
#[test]
pub fn failed_from_string() {
assert!(super::Date::try_from("2020-127-26").is_err());
}
#[cfg(test)]
#[test]
pub fn eta_yesterday() {
use chrono::Duration;
let future = Local::today() - Duration::days(1);
let date = Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), None);
#[test]
pub fn eta_tomorrow() {
let future = Local::today() + Duration::days(1);
let date = super::Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), Some(1));
}
#[test]
pub fn eta_today() {
let future = Local::today();
let date = super::Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), Some(0));
}
#[test]
pub fn eta_yesterday() {
let future = Local::today() - Duration::days(1);
let date = super::Date::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
)
.unwrap();
assert_eq!(date.eta(), None);
}
}

29
src/date_errors.rs

@ -0,0 +1,29 @@
/*
TU - Time's Up!
Copyright (C) 2020 Julio Biason
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#[derive(Debug, Eq, PartialEq)]
pub enum DateError {
/// The date is not valid
InvalidDate,
}
impl From<std::num::ParseIntError> for DateError {
fn from(_: std::num::ParseIntError) -> DateError {
DateError::InvalidDate
}
}

151
src/datetime.rs

@ -0,0 +1,151 @@
/*
TU - Time's Up!
Copyright (C) 2020 Julio Biason
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use chrono::prelude::*;
use chrono::LocalResult;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use crate::date_errors::DateError;
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct DateTime(chrono::DateTime<Local>);
impl DateTime {
pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8) -> Result<Self, DateError> {
match Local.ymd_opt(year as i32, month as u32, day as u32) {
LocalResult::None => Err(DateError::InvalidDate),
LocalResult::Ambiguous(_, _) => Err(DateError::InvalidDate),
LocalResult::Single(x) => match x.and_hms_opt(hour as u32, minute as u32, 59) {
Some(x) => Ok(DateTime(x)),
None => Err(DateError::InvalidDate),
},
}
}
pub fn year(&self) -> u16 {
self.0.date().year() as u16
}
pub fn month(&self) -> u8 {
self.0.date().month() as u8
}
pub fn day(&self) -> u8 {
self.0.date().day() as u8
}
pub fn hour(&self) -> u8 {
self.0.time().hour() as u8
}
pub fn minute(&self) -> u8 {
self.0.time().minute() as u8
}
/// Try to convert a string to a Date.
pub fn try_from(date: &str, time: &str) -> Result<Self, DateError> {
let mut date_frags = date.split("-");
let mut time_frags = time.split(":");
DateTime::new(
date_frags.next().ok_or(DateError::InvalidDate)?.parse()?,
date_frags.next().ok_or(DateError::InvalidDate)?.parse()?,
date_frags.next().ok_or(DateError::InvalidDate)?.parse()?,
time_frags.next().ok_or(DateError::InvalidDate)?.parse()?,
time_frags.next().ok_or(DateError::InvalidDate)?.parse()?,
)
}
pub fn eta(&self) -> Option<(u16, u16)> {
let diff = self.0 - Local::now();
let days = diff.num_days();
let hours = diff.num_hours() - (24 * diff.num_days());
if hours >= 0 {
Some((days as u16, hours as u16))
} else {
None
}
}
pub fn timestamp(&self) -> i64 {
self.0.timestamp()
}
}
#[cfg(test)]
mod datetime_test {
use chrono::prelude::*;
use chrono::Duration;
#[test]
pub fn invalid_date_time() {
assert!(super::DateTime::new(2020, 127, 2, 0, 0).is_err());
assert!(super::DateTime::new(2020, 6, 127, 0, 0).is_err());
assert!(super::DateTime::new(2020, 0, 0, 0, 0).is_err());
assert!(super::DateTime::new(2020, 6, 2, 24, 0).is_err());
assert!(super::DateTime::new(2020, 6, 2, 0, 60).is_err());
}
#[test]
pub fn valid_date_time() {
assert!(super::DateTime::new(2020, 6, 2, 20, 17).is_ok());
}
#[test]
pub fn from_string() {
if let Ok(x) = super::DateTime::try_from("2020-06-02", "20:18") {
assert_eq!(x.year(), 2020);
assert_eq!(x.month(), 6);
assert_eq!(x.day(), 2);
assert_eq!(x.hour(), 20);
assert_eq!(x.minute(), 18);
} else {
panic!("Can't parse 2020-06-02, 20:18");
}
}
#[test]
pub fn string_leading_zeroes() {
if let Ok(x) = super::DateTime::try_from("2020-09-09", "09:09") {
assert_eq!(x.year(), 2020);
assert_eq!(x.month(), 9);
assert_eq!(x.day(), 9);
assert_eq!(x.hour(), 9);
assert_eq!(x.minute(), 9);
} else {
panic!("Can't parse 2020-09-09, 09:09");
}
}
#[test]
pub fn eta_two_hours() {
let future = Local::now() + Duration::hours(2);
let datetime = super::DateTime::new(
future.year() as u16,
future.month() as u8,
future.day() as u8,
future.hour() as u8,
future.minute() as u8,
);
assert!(datetime.is_ok());
assert_eq!(datetime.unwrap().eta(), Some((0, 2)));
}
}

40
src/eventlist/event/eventtype.rs

@ -16,46 +16,24 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use chrono::prelude::*;
use chrono::DateTime;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use crate::eventlist::event::date;
use crate::eventlist::event::time;
use crate::date::Date;
use crate::datetime::DateTime;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "due", content = "datetime")]
pub enum EventType {
AllDay(date::Date),
AtTime(date::Date, time::Time),
AllDay(Date),
AtTime(DateTime),
}
impl From<&EventType> for DateTime<Local> {
fn from(origin: &EventType) -> Self {
match origin {
EventType::AllDay(d) => Local.ymd(d.year(), d.month(), d.day()).and_hms(23, 59, 59),
EventType::AtTime(d, t) => {
Local
.ymd(d.year(), d.month(), d.day())
.and_hms(t.hour(), t.minute(), 59)
}
}
}
}
impl From<&EventType> for String {
fn from(origin: &EventType) -> String {
match origin {
EventType::AllDay(d) => format!("{}{}{}0000", d.year(), d.month(), d.day()),
EventType::AtTime(d, t) => format!(
"{}{}{}{}{}",
d.year(),
d.month(),
d.day(),
t.hour(),
t.minute()
),
impl EventType {
pub fn timestamp(&self) -> i64 {
match self {
EventType::AllDay(date) => date.timestamp(),
EventType::AtTime(datetime) => datetime.timestamp(),
}
}
}

98
src/eventlist/event/mod.rs

@ -17,31 +17,25 @@
*/
use std::cmp::Ordering;
use std::convert::From;
use chrono::prelude::*;
use chrono::DateTime;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use uuid::Uuid;
mod date;
mod eventtype;
mod time;
use crate::date::Date;
use crate::datetime::DateTime;
pub mod eventtype;
use date::Date as EventDate;
use eventtype::EventType;
use time::Time as EventTime;
static DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
#[derive(Serialize, Deserialize, Debug)]
pub struct Event {
pub id: String,
pub description: String,
due: EventType,
pub due: EventType,
}
/// TODO inject this
fn uuid() -> String {
let (id, _, _, _) = Uuid::new_v4().as_fields();
format!("{:x}", id)
@ -53,57 +47,21 @@ pub enum EventError {
TooOld,
}
impl From<chrono::format::ParseError> for EventError {
fn from(error: chrono::format::ParseError) -> EventError {
EventError::InvalidDate(error.to_string())
}
}
impl Event {
pub fn new_on_date(description: &str, date: &str) -> Result<Self, EventError> {
let fake_datetime = format!("{} 00:00:00", date);
let dt = Local.datetime_from_str(&fake_datetime, DATE_FORMAT)?;
if dt < Local::now() {
Err(EventError::TooOld)
} else {
Ok(Self {
id: uuid(),
description: description.into(),
due: EventType::AllDay(EventDate::from(&dt)),
})
}
pub fn new_on_date(description: &str, date: &Date) -> Result<Self, EventError> {
Ok(Self {
id: uuid(),
description: description.into(),
due: EventType::AllDay(date.clone()),
})
}
pub fn new_on_date_time(description: &str, date: &str, time: &str) -> Result<Self, EventError> {
let fake_datetime = format!("{} {}:00", date, time);
let dt = Local.datetime_from_str(&fake_datetime, DATE_FORMAT)?;
if dt < Local::now() {
Err(EventError::TooOld)
} else {
Ok(Self {
id: uuid(),
description: description.into(),
due: EventType::AtTime(EventDate::from(&dt), EventTime::from(&dt)),
})
}
}
pub fn eta(&self) -> Option<String> {
let now = Local::now();
let to: DateTime<Local> = (&self.due).into();
let eta = to - now;
log::debug!("ETA for {}: {}", self.id, eta.num_minutes());
match self.due {
EventType::AllDay(_) if eta.num_minutes() > 0 => Some(format!("{}d", eta.num_days())),
EventType::AtTime(_, _) if eta.num_days() > 0 => {
Some(format!("{}d {}h", eta.num_days(), eta.num_hours()))
}
EventType::AtTime(_, _) if eta.num_hours() > 0 => Some(format!("{}h", eta.num_hours())),
_ => None,
}
pub fn new_on_date_time(description: &str, datetime: &DateTime) -> Result<Self, EventError> {
Ok(Self {
id: uuid(),
description: description.into(),
due: EventType::AtTime(datetime.clone()),
})
}
}
@ -111,26 +69,18 @@ impl Eq for Event {}
impl PartialEq for Event {
fn eq(&self, other: &Self) -> bool {
let self_str = String::from(&self.due);
let other_str = String::from(&other.due);
self_str == other_str
}
}
impl PartialOrd for Event {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_str = String::from(&self.due);
let other_str = String::from(&other.due);
Some(self_str.cmp(&other_str))
self.due.timestamp() == other.due.timestamp()
}
}
impl Ord for Event {
fn cmp(&self, other: &Self) -> Ordering {
let self_str = String::from(&self.due);
let other_str = String::from(&other.due);
self.due.timestamp().cmp(&other.due.timestamp())
}
}
self_str.cmp(&other_str)
impl PartialOrd for Event {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.due.timestamp().cmp(&other.due.timestamp()))
}
}

9
src/eventlist/eventlist.rs

@ -23,6 +23,8 @@ use serde_derive::Deserialize;
use serde_derive::Serialize;
use toml;
use crate::date::Date;
use crate::datetime::DateTime;
use crate::eventlist::event::Event;
use crate::eventlist::event::EventError;
@ -92,7 +94,7 @@ impl EventList {
/// Load the event list, add an all day event, and save it back.
/// Returns the ID of the new event.
pub fn add_event_with_date(description: &str, date: &str) -> Result<String, EventListError> {
pub fn add_event_with_date(description: &str, date: &Date) -> Result<String, EventListError> {
let mut list = EventList::load();
let event = Event::new_on_date(description, date)?;
let id = String::from(&event.id);
@ -105,11 +107,10 @@ impl EventList {
/// Returns the ID of the new event.
pub fn add_event_with_date_and_time(
description: &str,
date: &str,
time: &str,
datetime: &DateTime,
) -> Result<String, EventListError> {
let mut list = EventList::load();
let event = Event::new_on_date_time(description, date, time).unwrap();
let event = Event::new_on_date_time(description, datetime)?;
let id = String::from(&event.id);
list.push(event);
list.save();

36
src/main.rs

@ -20,8 +20,11 @@ use log;
mod args;
mod date;
mod date_errors;
mod datetime;
mod eventlist;
use crate::eventlist::event::eventtype::EventType;
use crate::eventlist::eventlist::EventList;
fn main() {
@ -32,12 +35,12 @@ fn main() {
match command {
args::Action::List => list(),
args::Action::Add(description, date) => {
let event_id = EventList::add_event_with_date(&description, "").unwrap();
let event_id = EventList::add_event_with_date(&description, &date).unwrap();
println!("Created new event {}", event_id);
}
args::Action::AddWithTime(description, date, time) => {
args::Action::AddWithTime(description, datetime) => {
let event_id =
EventList::add_event_with_date_and_time(&description, &date, &time).unwrap();
EventList::add_event_with_date_and_time(&description, &datetime).unwrap();
println!("Created new event {}", event_id);
}
}
@ -50,14 +53,27 @@ fn list() {
let event_list = EventList::load(); // TODO hide load from outside
println!("{:^8} | {:^7} | {}", "ID", "ETA", "Description");
// TODO: EventList::iter()
for record in event_list.into_iter() {
let eta = if let Some(eta) = record.eta() {
// TODO: "1d" == Tomorrow; "0d" == Today
eta
} else {
"Over".into()
for event in event_list.into_iter() {
let eta = match event.due {
EventType::AllDay(date) => {
let eta = date.eta();
match eta {
None => "Over".into(),
Some(0) => "Today".into(),
Some(1) => "Tomorrow".into(),
Some(x) => format!("{}d", x),
}
}
EventType::AtTime(datetime) => {
let eta = datetime.eta();
match eta {
None => "Over".into(),
Some((0, hours)) => format!("{}h", hours),
Some((days, hours)) => format!("{}d {}h", days, hours),
}
}
};
println!("{:>8} | {:>7} | {}", record.id, eta, record.description);
println!("{:>8} | {:>7} | {}", event.id, eta, event.description);
}
}

Loading…
Cancel
Save