twixter

A twtxt command line client in Rust

git clone git://git.shimmy1996.com/twixter.git

entry.rs (6942B)

    1 use chrono::{DateTime, Duration, FixedOffset, Local, SecondsFormat};
    2 
    3 use std::fmt;
    4 
    5 #[derive(Ord, PartialOrd, PartialEq, Eq, Debug)]
    6 pub struct Entry {
    7     timestamp: DateTime<FixedOffset>,
    8     author: Option<String>,
    9     content: String,
   10 }
   11 
   12 impl Entry {
   13     /// Generates a new entry from given content.
   14     pub fn new(content: String, author: Option<String>) -> Entry {
   15         Entry {
   16             timestamp: Self::now(),
   17             author,
   18             content,
   19         }
   20     }
   21 
   22     /// Attempt to parse given line into entry format.
   23     pub fn parse(line: &str, author: Option<&str>) -> Result<Entry, ()> {
   24         if let Some(seperator_idx) = line.find('\t') {
   25             if let Ok(timestamp) = DateTime::parse_from_rfc3339(&line[..seperator_idx]) {
   26                 Ok(Entry {
   27                     timestamp,
   28                     author: author.and_then(|x| Some(x.to_owned())),
   29                     content: line[seperator_idx + 1..].to_owned(),
   30                 })
   31             } else {
   32                 Err(())
   33             }
   34         } else {
   35             Err(())
   36         }
   37     }
   38 
   39     /// Generates string representation per twtxt spec for given entry.
   40     pub fn to_twtxt(&self) -> String {
   41         format!(
   42             "{}\t{}\n",
   43             &self.timestamp.to_rfc3339_opts(SecondsFormat::Secs, true),
   44             &self.content
   45         )
   46     }
   47 
   48     /// Formats a time duration in human readable format.
   49     /// Shows the first non-zero amount of year, month, week, day, hour, or
   50     /// minute. For duration shorter than one minute, return "just now".
   51     fn format_duration(duration: Duration) -> String {
   52         let (num, unit) = if duration.num_days() >= 365 {
   53             (duration.num_days() / 365, "year")
   54         } else if duration.num_days() >= 30 {
   55             (duration.num_days() / 30, "month")
   56         } else if duration.num_weeks() >= 1 {
   57             (duration.num_weeks(), "week")
   58         } else if duration.num_days() >= 1 {
   59             (duration.num_days(), "day")
   60         } else if duration.num_hours() >= 1 {
   61             (duration.num_hours(), "hour")
   62         } else if duration.num_minutes() >= 1 {
   63             (duration.num_minutes(), "minute")
   64         } else {
   65             return "just now".to_string();
   66         };
   67 
   68         if num > 1 {
   69             format!("{} {}s ago", num, unit)
   70         } else {
   71             format!("{} {} ago", num, unit)
   72         }
   73     }
   74 
   75     /// Get local time now as DateTime<FixedOffset>.
   76     fn now() -> DateTime<FixedOffset> {
   77         let local = Local::now();
   78         local.with_timezone(local.offset())
   79     }
   80 }
   81 
   82 impl fmt::Display for Entry {
   83     /// Formats a tweet for display in terminal.
   84     /// Alternate format uses absolute time.
   85     fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
   86         let timestamp = if formatter.alternate() {
   87             Self::format_duration(Local::now() - self.timestamp.with_timezone(&Local))
   88         } else {
   89             self.timestamp
   90                 .with_timezone(&Local)
   91                 .to_rfc3339_opts(SecondsFormat::Secs, true)
   92         };
   93 
   94         write!(
   95             formatter,
   96             "@{} {}\n{}",
   97             self.author.as_ref().unwrap_or(&"".to_string()),
   98             &timestamp,
   99             &self.content
  100         )
  101     }
  102 }
  103 
  104 #[cfg(test)]
  105 mod tests {
  106     use super::*;
  107 
  108     use chrono::offset::TimeZone;
  109 
  110     #[test]
  111     fn test_new() {
  112         let start: DateTime<FixedOffset> = Entry::now();
  113         let entry = Entry::new("content".to_string(), Some("author".to_string()));
  114         let end: DateTime<FixedOffset> = Entry::now();
  115 
  116         assert_eq!(start.cmp(&entry.timestamp), std::cmp::Ordering::Less);
  117         assert_eq!(end.cmp(&entry.timestamp), std::cmp::Ordering::Greater);
  118         assert_eq!(entry.content, "content");
  119         assert_eq!(entry.author, Some("author".to_string()));
  120     }
  121 
  122     #[test]
  123     fn test_parse() {
  124         assert_eq!(Entry::parse("This is not valid twtxt.\t", None), Err(()));
  125         assert_eq!(
  126             Entry::parse("2016-02-04T13:30:01+01:00\tThis is valid twtxt.", None),
  127             Ok(Entry {
  128                 timestamp: FixedOffset::east(3600).ymd(2016, 02, 04).and_hms(13, 30, 1),
  129                 author: None,
  130                 content: "This is valid twtxt.".to_string(),
  131             })
  132         );
  133     }
  134 
  135     #[test]
  136     fn test_to_twtxt() {
  137         assert_eq!(
  138             Entry {
  139                 timestamp: FixedOffset::east(3600)
  140                     .ymd(2016, 02, 04)
  141                     .and_hms_milli(13, 30, 1, 238),
  142                 author: None,
  143                 content: "Hello world!".to_string(),
  144             }
  145             .to_twtxt(),
  146             "2016-02-04T13:30:01+01:00\tHello world!\n"
  147         );
  148     }
  149 
  150     #[test]
  151     fn test_format_duration() {
  152         assert_eq!(
  153             Entry::format_duration(Duration::days(365 * 2)),
  154             "2 years ago"
  155         );
  156         assert_eq!(Entry::format_duration(Duration::days(365)), "1 year ago");
  157         assert_eq!(
  158             Entry::format_duration(Duration::days(30 * 3)),
  159             "3 months ago"
  160         );
  161         assert_eq!(Entry::format_duration(Duration::days(30)), "1 month ago");
  162         assert_eq!(Entry::format_duration(Duration::weeks(4)), "4 weeks ago");
  163         assert_eq!(Entry::format_duration(Duration::weeks(1)), "1 week ago");
  164         assert_eq!(Entry::format_duration(Duration::days(4)), "4 days ago");
  165         assert_eq!(Entry::format_duration(Duration::days(1)), "1 day ago");
  166         assert_eq!(Entry::format_duration(Duration::hours(23)), "23 hours ago");
  167         assert_eq!(Entry::format_duration(Duration::hours(1)), "1 hour ago");
  168         assert_eq!(
  169             Entry::format_duration(Duration::minutes(5)),
  170             "5 minutes ago"
  171         );
  172         assert_eq!(Entry::format_duration(Duration::minutes(1)), "1 minute ago");
  173         assert_eq!(Entry::format_duration(Duration::seconds(30)), "just now");
  174     }
  175 
  176     #[test]
  177     fn test_now() {
  178         let to_fixed_offset = |x: &DateTime<Local>| x.with_timezone(x.offset());
  179 
  180         let start = to_fixed_offset(&Local::now());
  181         let timestamp = Entry::now();
  182         let end = to_fixed_offset(&Local::now());
  183 
  184         assert_eq!(timestamp.offset(), Local::now().offset());
  185         assert_eq!(start.cmp(&timestamp), std::cmp::Ordering::Less);
  186         assert_eq!(end.cmp(&timestamp), std::cmp::Ordering::Greater);
  187     }
  188 
  189     #[test]
  190     fn test_display() {
  191         let timestamp = Entry::now() - Duration::weeks(1);
  192         let entry = Entry {
  193             timestamp,
  194             author: Some("anonymous".to_string()),
  195             content: "Hello world!".to_string(),
  196         };
  197 
  198         assert_eq!(
  199             format!("{:#}", entry),
  200             "@anonymous 1 week ago\nHello world!"
  201         );
  202 
  203         assert_eq!(
  204             format!("{}", entry),
  205             format!(
  206                 "@anonymous {}\nHello world!",
  207                 timestamp.to_rfc3339_opts(SecondsFormat::Secs, true)
  208             )
  209         );
  210     }
  211 }