Skip to content
Snippets Groups Projects
Unverified Commit 03159064 authored by ck85nori's avatar ck85nori :railway_track: Committed by ck85nori
Browse files

rewrites it in rust

parent c248f2f3
No related branches found
No related tags found
No related merge requests found
/target
*.db
*.csv
max_width = 79
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time",
"winapi",
]
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "csv"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "libsqlite3-sys"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "pkg-config"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]]
name = "prettytable-rs"
version = "0.8.0"
source = "git+https://github.com/wookietreiber/prettytable-rs?branch=wookieepatch#49282808555841a3b5b41867bf81524de116fca5"
dependencies = [
"atty",
"csv",
"encode_unicode",
"lazy_static",
"term",
"unicode-width",
]
[[package]]
name = "proc-macro2"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "rusqlite"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57adcf67c8faaf96f3248c2a7b419a0dbc52ebe36ba83dd57fe83827c1ea4eb3"
dependencies = [
"bitflags",
"chrono",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
[[package]]
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "serde"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "smallvec"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "smooth"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455f1ef596f9ea938a71ea5615bcd6be4148ea10e8892e665de6d70936b40248"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "ttanalysis"
version = "0.1.0"
dependencies = [
"anyhow",
"atty",
"chrono",
"clap",
"csv",
"prettytable-rs",
"rusqlite",
"serde",
"serde_json",
"smooth",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[package]
name = "ttanalysis"
version = "0.1.0"
authors = ["Christian Krause <christian.krause@mailbox.org>"]
edition = "2018"
[dependencies]
anyhow = "1"
atty = "0.2"
chrono = { version = "0.4", features = ["serde"] }
clap = "2"
csv = "1"
rusqlite = { version = "0.25", features = ["chrono"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
smooth = "0.1"
[dependencies.prettytable-rs]
git = "https://github.com/wookietreiber/prettytable-rs"
branch = "wookieepatch"
# Time Tracking Analysis
Analyze time tracking data.
## Dependencies
- JDK 8
- SQLite 3
- [Ammonite](http://www.lihaoyi.com/Ammonite/#Ammonite-REPL)
## Usage
1. Create a database for your **debit** work hours:
```
sqlite3 timetracking.db 'create table debit (day text primary key, hours real)'
```
2. Fill the database with your **debit** work hours:
```
sqlite3 timetracking.db 'insert into debit (day, hours) values ("2017-03-28", 8)'
```
3. Start analysis:
```
amm ttanalysis.sc timetracking.db export_all.csv
```
use std::path::Path;
use anyhow::Result;
use chrono::IsoWeek;
use prettytable::format::FormatBuilder;
use prettytable::{cell, row, Cell, Table};
use smooth::{MultiSmooth, Smooth};
use crate::cli::Color;
pub fn run<P1, P2>(gleeo: P1, sqlite: P2, color: Color) -> Result<()>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let debit = crate::debit::all(sqlite)?.weekly();
let credit = crate::credit::all(gleeo)?.weekly();
let mut weeks: Vec<IsoWeek> = vec![];
weeks.extend(debit.keys());
weeks.extend(credit.keys());
weeks.sort_unstable();
weeks.dedup();
let mut credit_weekly = Vec::with_capacity(weeks.len());
let mut debit_weekly = Vec::with_capacity(weeks.len());
let mut balance_weekly = Vec::with_capacity(weeks.len());
let mut rolling_weekly = Vec::with_capacity(weeks.len());
let mut total_credit = f64::default();
let mut total_debit = f64::default();
let mut total_balance = f64::default();
for week in &weeks {
let credit = credit.get(week).copied().unwrap_or_default();
// ALLOW time-tracked entries won't be too large
#[allow(clippy::cast_precision_loss)]
let credit = (credit as f64) / 3600.0;
credit_weekly.push(credit);
let debit = debit.get(week).copied().unwrap_or_default();
debit_weekly.push(debit);
let balance = credit - debit;
balance_weekly.push(balance);
total_credit += credit;
total_debit += debit;
total_balance += balance;
rolling_weekly.push(total_balance);
}
credit_weekly.smooth_mut();
balance_weekly.smooth_mut();
rolling_weekly.smooth_mut();
let mut table = Table::new();
let format = FormatBuilder::new().column_separator(' ').build();
table.set_format(format);
table.set_titles(
row![bu->"Week", bu->"Debit", bu->"Credit", bu->"Balance", bu->"Rolling"],
);
for (index, week) in weeks.iter().enumerate() {
let mut row = row![
format!("{:?}", week),
r->debit_weekly[index],
r->credit_weekly[index]
];
row.add_cell(color_by_balance(balance_weekly[index]));
row.add_cell(color_by_balance(rolling_weekly[index]));
table.add_row(row);
}
if table.print_tty(color.with_atty()).is_ok() {
println!();
println!("total debit: {} hours", total_debit.smooth());
println!("total credit: {} hours", total_credit.smooth());
println!();
println!("balance: {}", total_balance.smooth());
println!();
}
Ok(())
}
fn color_by_balance(a: f64) -> Cell {
let cell = cell!(a);
let style = if a.is_sign_positive() {
"Fgr"
} else if a.is_sign_negative() {
"Frr"
} else {
"r"
};
cell.style_spec(style)
}
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use anyhow::Result;
use atty::Stream;
use clap::{crate_description, crate_name, crate_version};
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches};
pub enum Mode {
Balance,
ListCredit,
ListDebit,
}
#[derive(Clone, Copy, Debug)]
pub enum Color {
Yes,
No,
Always,
}
impl Color {
pub fn with_atty(self) -> bool {
match self {
Color::Yes => atty::is(Stream::Stdout),
Color::No => false,
Color::Always => true,
}
}
}
pub struct Arguments {
pub mode: Mode,
pub color: Color,
pub gleeo: Option<PathBuf>,
pub sqlite: Option<PathBuf>,
}
impl TryFrom<ArgMatches<'_>> for Arguments {
type Error = anyhow::Error;
fn try_from(args: ArgMatches) -> Result<Self, Self::Error> {
let mode = if args.is_present("list-credit") {
Mode::ListCredit
} else if args.is_present("list-debit") {
Mode::ListDebit
} else {
Mode::Balance
};
let color = match args.value_of("color").unwrap() {
"yes" => Color::Yes,
"always" => Color::Always,
"no" => Color::No,
_ => unreachable!("clap::Arg::possible_values"),
};
let gleeo = args.value_of("gleeo").map(|p| Path::new(p).into());
let sqlite = args.value_of("sqlite").map(|p| Path::new(p).into());
Ok(Self {
mode,
color,
gleeo,
sqlite,
})
}
}
pub fn args() -> Result<Arguments> {
let colorize = atty::is(Stream::Stdout);
let cli = build(colorize);
let args = cli.get_matches();
let arguments = Arguments::try_from(args)?;
Ok(arguments)
}
pub fn build(colorize: bool) -> App<'static, 'static> {
let colorize = if colorize {
AppSettings::ColoredHelp
} else {
AppSettings::ColorNever
};
let list_credit = Arg::with_name("list-credit")
.long("list-credit")
.help("list credit");
let list_debit = Arg::with_name("list-debit")
.long("list-debit")
.help("list debit");
let mode = ArgGroup::with_name("mode")
.arg("list-credit")
.arg("list-debit");
let color = Arg::with_name("color")
.long("color")
.help("control ANSI color escapes")
.takes_value(true)
.possible_values(&["yes", "no", "always"])
.default_value("yes");
let sqlite = Arg::with_name("sqlite")
.long("debit")
.takes_value(true)
.help("sqlite debit file")
.required_unless("list-credit");
let gleeo = Arg::with_name("gleeo")
.long("gleeo")
.takes_value(true)
.help("gleeo credit file")
.required_unless("list-debit");
App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
.global_setting(colorize)
.help_short("?")
.help_message("show this help output")
.version_message("show version")
.arg(list_credit)
.arg(list_debit)
.group(mode)
.arg(color)
.arg(sqlite)
.arg(gleeo)
}
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Local};
use super::Entry;
pub fn from_path<P>(path: P) -> Result<Vec<Entry>>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let mut reader = csv::Reader::from_path(path)
.with_context(|| format!("parsing {}", path.display()))?;
let headers = reader
.headers()
.with_context(|| format!("parsing headers in {}", path.display()))?;
let duration_index = headers
.iter()
.position(|h| h == "Decimal Duration")
.ok_or_else(|| {
anyhow!("no 'Decimal Duration' column in {}", path.display())
})?;
let start_index = headers
.iter()
.position(|h| h == "Start")
.ok_or_else(|| anyhow!("no 'Start' column in {}", path.display()))?;
let tz_index =
headers
.iter()
.position(|h| h == "TimeZone")
.ok_or_else(|| {
anyhow!("no 'TimeZone' column in {}", path.display())
})?;
let mut credit = vec![];
for record in reader.records().enumerate() {
let (record_index, record) = record;
// bump record index so it starts at 1 (readability)
let record_index = record_index + 1;
let record = record
.with_context(|| format!("parsing record {}", record_index))?;
let hours = record.get(duration_index).ok_or_else(|| {
anyhow!("no 'Decimal Duration' field in record {}", record_index)
})?;
let hours = hours.parse::<f64>().with_context(|| {
format!(
"can't parse {:?} as f64 in record {}",
hours, record_index
)
})?;
// ALLOW time-tracked entries won't be too large
#[allow(clippy::cast_possible_truncation)]
let seconds = (hours * 3600.0) as i64;
let start = record.get(start_index).ok_or_else(|| {
anyhow!("no 'Start' field in record {}", record_index)
})?;
let tz = record.get(tz_index).ok_or_else(|| {
anyhow!("no 'TimeZone' field in record {}", record_index)
})?;
let tz = match tz.strip_prefix("UTC") {
Some(tz) => tz,
None => tz,
};
let tz = if tz.len() == 5 {
tz.replace("+", "+0")
} else {
tz.into()
};
let start = format!("{} {}", start, tz);
let dt = DateTime::parse_from_str(&start, "%Y-%m-%d %H:%M %:z")
.with_context(|| format!("parsing start date {}", start))?;
let dt: DateTime<Local> = DateTime::from(dt);
let date = dt.date();
credit.push(Entry { date, seconds });
}
Ok(credit)
}
#[cfg(test)]
mod tests {
#[test]
fn decimal_duration() {
let s = "0.3333333333333333";
let h = s.parse::<f64>().unwrap();
let seconds = (h * 3600.0) as i64;
assert_eq!(seconds, 1200);
}
}
pub mod gleeo;
pub mod timew;
use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
use chrono::{Date, Datelike, IsoWeek, Local};
use prettytable::format::FormatBuilder;
use prettytable::{cell, row, Table};
use crate::cli::Color;
pub struct Credit {
entries: Vec<Entry>,
}
impl Credit {
pub fn weekly(&self) -> BTreeMap<IsoWeek, i64> {
let mut data = BTreeMap::default();
for entry in &self.entries {
let week = entry.date.iso_week();
*data.entry(week).or_insert(0) += entry.seconds;
}
data
}
}
pub struct Entry {
date: Date<Local>,
seconds: i64,
}
impl Entry {
pub const fn date(&self) -> &Date<Local> {
&self.date
}
pub const fn seconds(&self) -> i64 {
self.seconds
}
}
pub fn all<P>(gleeo: P) -> Result<Credit>
where
P: AsRef<Path>,
{
let mut entries = vec![];
let gleeo = gleeo::from_path(gleeo.as_ref())?;
let timew = timew::from_timew_export()?;
entries.extend(gleeo);
entries.extend(timew);
Ok(Credit { entries })
}
pub fn print<P>(gleeo: P, color: Color) -> Result<()>
where
P: AsRef<Path>,
{
let credit = all(gleeo)?;
let mut table = Table::new();
let format = FormatBuilder::new().column_separator(' ').build();
table.set_format(format);
table.set_titles(row![bu->"Date", bu->"Seconds"]);
for entry in credit.entries {
table.add_row(row![entry.date(), r->entry.seconds()]);
}
let _ignored = table.print_tty(color.with_atty());
Ok(())
}
use std::io::Read;
use std::process::{Command, Stdio};
use anyhow::{Context, Result};
use chrono::{DateTime, Local, NaiveDateTime, Utc};
use serde::{Deserialize, Deserializer};
use super::Entry;
pub fn from_timew_export() -> Result<Vec<Entry>> {
let mut child = Command::new("timew")
.arg("export")
.stdout(Stdio::piped())
.spawn()
.with_context(|| "running 'timew export'")?;
let stdout = child
.stdout
.take()
.with_context(|| "take 'timew export' output")?;
let credit = from_reader(stdout)?;
child.wait().with_context(|| "waiting on 'timew export'")?;
Ok(credit)
}
pub fn from_reader<R>(r: R) -> Result<Vec<Entry>>
where
R: Read,
{
let timew: Vec<Data> = serde_json::from_reader(r)?;
let credit = timew
.into_iter()
.filter_map(|timew| {
timew.end.map(|end| {
let duration = end - timew.start;
Entry {
date: timew.start.date(),
seconds: duration.num_seconds(),
}
})
})
.collect();
Ok(credit)
}
// ----------------------------------------------------------------------------
// serde helper
// ----------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
pub struct Data {
id: usize,
#[serde(deserialize_with = "timew_to_local")]
start: DateTime<Local>,
#[serde(default)]
#[serde(deserialize_with = "timew_end_to_local")]
end: Option<DateTime<Local>>,
#[serde(default)]
tags: Vec<String>,
annotation: Option<String>,
}
fn timew_to_local<'de, D>(deserializer: D) -> Result<DateTime<Local>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let dt = NaiveDateTime::parse_from_str(&s, "%Y%m%dT%H%M%SZ")
.map_err(serde::de::Error::custom)?;
let dt = DateTime::<Utc>::from_utc(dt, Utc);
let dt = DateTime::from(dt);
Ok(dt)
}
fn timew_end_to_local<'de, D>(
deserializer: D,
) -> Result<Option<DateTime<Local>>, D::Error>
where
D: Deserializer<'de>,
{
timew_to_local(deserializer).map(Some)
}
pub mod sqlite;
use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
use chrono::naive::NaiveDate;
use chrono::{Datelike, IsoWeek};
use prettytable::format::FormatBuilder;
use prettytable::{cell, row, Table};
use crate::cli::Color;
pub struct Debit {
entries: Vec<Entry>,
}
impl Debit {
pub fn weekly(&self) -> BTreeMap<IsoWeek, f64> {
let mut data = BTreeMap::default();
for entry in &self.entries {
let week = entry.date.iso_week();
*data.entry(week).or_insert(0.0) += entry.hours;
}
data
}
}
#[derive(Debug)]
pub struct Entry {
date: NaiveDate,
hours: f64,
}
impl Entry {
pub const fn date(&self) -> &NaiveDate {
&self.date
}
pub const fn hours(&self) -> f64 {
self.hours
}
}
pub fn all<P>(sqlite: P) -> Result<Debit>
where
P: AsRef<Path>,
{
let entries = sqlite::from_path(sqlite)?;
Ok(Debit { entries })
}
pub fn print<P>(path: P, color: Color) -> Result<()>
where
P: AsRef<Path>,
{
let debit = all(path)?;
let mut table = Table::new();
let format = FormatBuilder::new().column_separator(' ').build();
table.set_format(format);
table.set_titles(row![bu->"Date", bu->"Hours"]);
for entry in debit.entries {
table.add_row(row![entry.date(), r->entry.hours()]);
}
let _ignored = table.print_tty(color.with_atty());
Ok(())
}
use std::path::Path;
use rusqlite::{Connection, Result};
use super::Entry;
pub fn from_path<P>(path: P) -> Result<Vec<Entry>>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let conn = Connection::open(path)?;
let mut stmt = conn.prepare("SELECT day, hours FROM debit")?;
let iter = stmt.query_map([], |row| {
let date = row.get(0)?;
let hours = row.get(1)?;
Ok(Entry { date, hours })
})?;
Ok(iter.map(Result::unwrap).collect())
}
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
mod balance;
mod cli;
mod credit;
mod debit;
use anyhow::Result;
use self::cli::Mode;
fn main() -> Result<()> {
let args = cli::args()?;
match args.mode {
Mode::ListCredit => credit::print(args.gleeo.unwrap(), args.color),
Mode::ListDebit => debit::print(args.sqlite.unwrap(), args.color),
Mode::Balance => {
balance::run(args.gleeo.unwrap(), args.sqlite.unwrap(), args.color)
}
}
}
#!/usr/bin/env amm
import $ivy.`com.github.wookietreiber::scala-chart:0.5.1`, scalax.chart.api._
import $ivy.`com.github.tototoshi::scala-csv:1.3.5`, com.github.tototoshi.csv._
import $ivy.`org.typelevel::cats-core:1.1.0`
import cats._
import cats.instances.all._
import cats.syntax.all._
import java.time._
import sys.process._
import ammonite.ops._
val weekFields = temporal.WeekFields.of(java.util.Locale.getDefault)
// -------------------------------------------------------------------------------------------------
// debit
// -------------------------------------------------------------------------------------------------
class Debit(path: Path) {
val entries: List[(LocalDate,Double)] = for {
entry <- Seq("sqlite3", path.toNIO.toString, "select * from debit").lineStream.toList
parts = entry.split("\\|")
date = LocalDate.parse(parts(0))
hours = parts(1).toDouble
} yield date -> hours
val total = entries.foldLeft(0.0)(_ + _._2.toDouble)
val weekly: Map[String,List[Double]] = entries.groupBy({
case (date,_) =>
val year = date.getYear
val week = date.get(weekFields.weekOfWeekBasedYear)
f"$year%d-$week%02d"
}).mapValues(_.map(_._2))
def summary(): Unit = for {
(week,numbers) <- weekly.toSeq.sortBy(_._1)
hours = numbers.sum
} println(s"$week $hours")
}
// -------------------------------------------------------------------------------------------------
// credit
// -------------------------------------------------------------------------------------------------
class Credit(path: Path) {
case class TTEntry(start: LocalDateTime, end: LocalDateTime, duration: Double) {
override def toString: String = {
val d = (duration * 1000).round / 1000.0
s"""from $start to $end (duration=$d hours)"""
}
}
val entries = {
val reader = CSVReader.open(path.toIO)
val ttraw: List[List[String]] = reader.all().drop(1)
val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val ttx: List[TTEntry] = for {
entry <- ttraw
start = LocalDateTime.parse(entry(4), formatter)
end = LocalDateTime.parse(entry(5), formatter)
duration = entry(8).toDouble
} yield TTEntry(start, end, duration)
reader.close()
ttx
}
val total = entries.foldLeft(0.0)(_ + _.duration)
val weekly: Map[String,List[Double]] = entries.groupBy({ x =>
val year = x.start.getYear
val week = x.start.get(weekFields.weekOfWeekBasedYear)
f"$year%d-$week%02d"
}).mapValues(_.map(_.duration))
def summary: Unit = for {
(week,numbers) <- weekly.toSeq.sortBy(_._1)
hours = numbers.sum
} println(s"$week $hours")
}
// -------------------------------------------------------------------------------------------------
// balance
// -------------------------------------------------------------------------------------------------
class Balance(debit: Debit, credit: Credit) {
case class Rolling(week: String, debit: Double, credit: Double, balance: Double, change: Double)
def rolling: Seq[Rolling] = {
val xx: Map[String,List[Double]] =
debit.weekly.mapValues(_.sum).mapValues(_ :: Nil) |+|
credit.weekly.mapValues(_.sum).mapValues(_ :: Nil)
var b = 0.0
for {
(week,List(debit,credit)) <- xx.toSeq.sortBy(_._1)
change = credit - debit
} yield {
b += change
Rolling(week, debit, credit, b, change)
}
}
}
// -------------------------------------------------------------------------------------------------
// balance
// -------------------------------------------------------------------------------------------------
@main
def main(debitIn: Path, creditIn: Path) = {
val debit = new Debit(debitIn)
val credit = new Credit(creditIn)
val balance = new Balance(debit, credit)
for (r <- balance.rolling) {
val week = r.week
val debit = r.debit
val credit = r.credit
val balance = r.balance
val change = r.change
val cc = if (change < 0)
f"${Console.RED}${change.round}%5d${Console.RESET}"
else
f"${Console.GREEN}${change.round}%5d${Console.RESET}"
val cb = if (balance < 0)
f"${Console.RED}${balance.round}%5d${Console.RESET}"
else
f"${Console.GREEN}${balance.round}%5d${Console.RESET}"
println(f"$week ${debit.round}%5d ${credit.round}%5d $cc $cb")
}
println(s"""|
|total debit: ${debit.total} hours
|total credit: ${credit.total} hours
|
|balance: ${credit.total - debit.total} hours
|""".stripMargin)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment