local timezone and timetrap database management

This commit is contained in:
Abraham Toriz 2021-06-29 18:20:51 -05:00
parent bd10e8582a
commit af2a18c8db
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
6 changed files with 113 additions and 35 deletions

1
Cargo.lock generated
View File

@ -469,6 +469,7 @@ dependencies = [
name = "tiempo"
version = "0.1.0"
dependencies = [
"ansi_term 0.12.1",
"chrono",
"clap",
"dirs",

View File

@ -18,6 +18,7 @@ toml = "0.5"
itertools = "0.10"
textwrap = "0.14"
terminal_size = "0.1"
ansi_term = "0.12"
[dev-dependencies]
pretty_assertions = "0.7.2"

View File

@ -3,16 +3,44 @@ use std::io::Write;
use std::str::FromStr;
use clap::ArgMatches;
use chrono::{DateTime, Utc, Local, TimeZone};
use chrono::{DateTime, Utc, Local, TimeZone, LocalResult};
use terminal_size::{Width, terminal_size};
use ansi_term::Color::Yellow;
use crate::error;
use crate::database::Database;
use crate::database::{Database, DBVersion};
use crate::formatters::Formatter;
use crate::config::Config;
use crate::models::Entry;
use super::Command;
fn local_to_utc(t: DateTime<Utc>) -> error::Result<DateTime<Utc>> {
let local_time = match Local.from_local_datetime(&t.naive_utc()) {
LocalResult::None => return Err(error::Error::NoneLocalTime(t.naive_utc())),
LocalResult::Single(t) => t,
LocalResult::Ambiguous(t1, t2) => return Err(error::Error::AmbiguousLocalTime {
orig: t.naive_utc(),
t1, t2,
}),
};
Ok(Utc.from_utc_datetime(&local_time.naive_utc()))
}
fn local_to_utc_vec(entries: Vec<Entry>) -> error::Result<Vec<Entry>> {
entries
.into_iter()
.map(|e| {
Ok(Entry {
start: local_to_utc(e.start)?,
end: e.end.map(|t| local_to_utc(t)).transpose()?,
..e
})
})
.collect()
}
fn term_width() -> usize {
if let Some((Width(w), _)) = terminal_size() {
w as usize
@ -64,8 +92,7 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
}
}
pub struct DisplayCommand {
}
pub struct DisplayCommand { }
impl<'a> Command<'a> for DisplayCommand {
type Args = Args;
@ -76,7 +103,7 @@ impl<'a> Command<'a> for DisplayCommand {
O: Write,
E: Write,
{
let sheets_to_display = match args.sheet {
let entries = match args.sheet {
Some(Sheet::All) => db.entries_all_visible()?,
Some(Sheet::Full) => db.entries_full()?,
Some(Sheet::Sheet(name)) => db.entries_by_sheet(&name)?,
@ -87,11 +114,25 @@ impl<'a> Command<'a> for DisplayCommand {
}
};
let entries = if let DBVersion::Timetrap = db.version()? {
// this indicates that times in the database are specified in the
// local time and need to be converted to utc before displaying
writeln!(
err,
"{} You are using the old timetrap format, it is advised that \
you update your database using t migrate",
Yellow.bold().paint("[WARNING]"),
)?;
local_to_utc_vec(entries)?
} else {
entries
};
args.format.print_formatted(
sheets_to_display,
entries,
out,
Utc::now(),
Local.offset_from_utc_datetime(&Utc::now().naive_utc()),
args.ids,
term_width(),
)
@ -119,13 +160,13 @@ mod tests {
Day Start End Duration Notes
Tue Jun 29, 2021 06:26:49 - 07:26:52 1:00:03 lets do some rust
1:00:03
---------------------------------------------------------------------
-------------------------------------------------------------------
Total 1:00:03
"));
assert_eq!(
PrettyString(&String::from_utf8_lossy(&err)),
PrettyString("[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate")
String::from_utf8_lossy(&err),
format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")),
);
}
@ -134,4 +175,9 @@ mod tests {
assert!(false, "start with a newly created database");
assert!(false, "correctly display times in local timezone");
}
#[test]
fn query_by_local_time_unless_timezone_specified() {
assert!(false, "when using --start and --end times are assumed to be given in the local time unless they parse to a timezone aware time");
}
}

View File

@ -6,6 +6,11 @@ use chrono::{DateTime, Utc};
use crate::error;
use crate::models::{Entry, Meta};
pub enum DBVersion {
Timetrap,
Version(u16),
}
pub trait Database {
/// This is used to create tables and insert rows
fn execute(&mut self, query: &str, params: &[&dyn ToSql]) -> error::Result<()>;
@ -34,10 +39,25 @@ pub trait Database {
// Meta queries
fn current_sheet(&self) -> error::Result<Option<String>> {
let results = self.meta_query("select * from meta where key=?1", &[&"current_sheet"])?;
let results = self.meta_query("select * from meta where key='current_sheet'", &[])?;
Ok(results.into_iter().next().map(|m| m.value))
}
fn version(&self) -> error::Result<DBVersion> {
let results = self.meta_query("select * from meta where key='database_version'", &[])?;
if let Some(v) = results.into_iter().next().map(|m| m.value) {
Ok(DBVersion::Version(v.parse().map_err(|e| {
error::Error::CorruptedData(format!(
"Found value '{}' for key 'database_version' in meta table, which is not a valid integer",
v
))
})?))
} else {
Ok(DBVersion::Timetrap)
}
}
}
pub struct SqliteDatabase {

View File

@ -2,6 +2,7 @@ use std::result;
use std::path::PathBuf;
use thiserror::Error;
use chrono::{NaiveDateTime, DateTime, Local};
#[derive(Debug, Error)]
pub enum Error {
@ -30,7 +31,20 @@ pub enum Error {
DateTimeParseError(#[from] chrono::format::ParseError),
#[error("IOError: {0}")]
IOError(#[from] std::io::Error)
IOError(#[from] std::io::Error),
#[error("Corrupted data found in the database: {0}")]
CorruptedData(String),
#[error("Trying to parse {0} as a time in your timezone led to no results")]
NoneLocalTime(NaiveDateTime),
#[error("Trying to parse {orig} as a time in your timezone led to the ambiguous results {t1} and {t2}")]
AmbiguousLocalTime {
orig: NaiveDateTime,
t1: DateTime<Local>,
t2: DateTime<Local>,
}
}
pub type Result<T> = result::Result<T, Error>;

View File

@ -5,8 +5,8 @@ use std::borrow::Cow;
use serde::{Serialize, Deserialize};
use itertools::Itertools;
use chrono::{
DateTime, Utc, Offset, TimeZone, Duration, NaiveTime, Timelike,
NaiveDateTime,
DateTime, Utc, TimeZone, Duration, NaiveTime, Timelike, NaiveDateTime,
Local,
};
use crate::error;
@ -70,9 +70,9 @@ impl Formatter {
/// to the local timezone is given in `offset` to prevent this function from
/// using a secondary effect to retrieve the time and conver dates. This
/// also makes it easier to test.
pub fn print_formatted<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
pub fn print_formatted<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
match &self {
Formatter::Text => self.print_formatted_text(entries, out, now, offset, ids, term_width)?,
Formatter::Text => self.print_formatted_text(entries, out, now, ids, term_width)?,
Formatter::Custom(name) => {
}
}
@ -82,14 +82,9 @@ impl Formatter {
/// Print in the default text format. Assume entries are sorted by sheet and
/// then by start
fn print_formatted_text<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
fn print_formatted_text<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
// Build a timezone based on the offset given to this function. This
// will later be used to properly group the entries by date in the local
// timezone.
let fixed_offset = offset.fix();
for (key, group) in grouped_entries.into_iter() {
writeln!(out, "Timesheet: {}", key)?;
@ -97,18 +92,18 @@ impl Formatter {
// A vector of lines to be printed, with all the components
let mut lines = Vec::new();
let entries_by_date = group.group_by(|e| fixed_offset.from_utc_datetime(&e.start.naive_utc()).date());
let entries_by_date = group.group_by(|e| Local.from_utc_datetime(&e.start.naive_utc()).date());
let mut total = Duration::seconds(0);
for (date, entries) in entries_by_date.into_iter() {
let mut daily = Duration::seconds(0);
for (i, entry) in entries.into_iter().enumerate() {
let start = format_start(fixed_offset.from_utc_datetime(&entry.start.naive_utc()).time());
let start = format_start(Local.from_utc_datetime(&entry.start.naive_utc()).time());
let end = entry.end.map(|t| {
format_end(
fixed_offset.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
fixed_offset.from_utc_datetime(&t.naive_utc()).naive_local()
Local.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
Local.from_utc_datetime(&t.naive_utc()).naive_local()
)
}).unwrap_or(" ".into());
let duration = entry.end.unwrap_or(now) - entry.start;
@ -231,6 +226,7 @@ mod tests {
#[test]
fn test_constrained_lines_long_text() {
std::env::set_var("TZ", "UTC");
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
"chatting with bob about upcoming task,",
"district sharing of images, how the user",
@ -245,6 +241,7 @@ mod tests {
#[test]
fn test_text_output() {
std::env::set_var("TZ", "UTC");
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
@ -255,9 +252,8 @@ mod tests {
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -274,6 +270,7 @@ mod tests {
#[test]
fn test_text_output_with_millis() {
std::env::set_var("TZ", "UTC");
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
@ -281,9 +278,8 @@ mod tests {
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -306,7 +302,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -333,7 +329,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, true, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
ID Day Start End Duration Notes
@ -365,7 +361,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -400,7 +396,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, true, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
ID Day Start End Duration Notes
@ -435,7 +431,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes