From 1423ecbd30f243637ea75a0aac3f0d19b0b8d18e Mon Sep 17 00:00:00 2001 From: Till Höppner Date: Thu, 10 Mar 2016 15:19:45 +0100 Subject: Various CLI improvements related to timezones --- cli/src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++-------------- ops/Cargo.toml | 1 + ops/src/lib.rs | 1 + ops/src/stats.rs | 26 ++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 8eacea4..eb2fb43 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -52,15 +52,17 @@ pub fn main(cli: Cli) { let args = App::new("ilc") .version(&version[..]) .setting(AppSettings::GlobalVersion) + .setting(AppSettings::AllowLeadingHyphen) + .setting(AppSettings::UnifiedHelpMessage) .setting(AppSettings::VersionlessSubcommands) .setting(AppSettings::ArgRequiredElseHelp) .author("Till Höppner ") .about("A converter and statistics utility for IRC log files") - .arg(Arg::with_name("timezone") - .help("UTC offset in the direction of the western hemisphere") + .arg(Arg::with_name("time") + .help("Timestamp offset, in seconds") .global(true) .takes_value(true) - .long("timezone") + .long("timeoffset") .short("t")) .arg(Arg::with_name("date") .help("Override the date for this log, ISO 8601, YYYY-MM-DD") @@ -101,6 +103,7 @@ pub fn main(cli: Cli) { .global(true) .takes_value(true) .multiple(true) + .number_of_values(1) .long("input") .short("i")) .arg(Arg::with_name("output_file") @@ -114,24 +117,32 @@ pub fn main(cli: Cli) { .takes_value(false) .long("notice")) .subcommand(SubCommand::with_name("parse") - .about("Parse the input, checking the format")) + .about("Parse the input, checking the format") + .setting(AppSettings::AllowLeadingHyphen)) .subcommand(SubCommand::with_name("convert") - .about("Convert from a source to a target format")) + .about("Convert from a source to a target format") + .setting(AppSettings::AllowLeadingHyphen)) .subcommand(SubCommand::with_name("stats") - .about("Analyse the activity of users by certain metrics")) + .about("Analyse the activity of users by certain metrics") + .setting(AppSettings::AllowLeadingHyphen)) .subcommand(SubCommand::with_name("seen") .about("Print the last line a nick was active") + .setting(AppSettings::AllowLeadingHyphen) .arg(Arg::with_name("nick") .help("The nick you're looking for") .takes_value(true) .required(true) .index(1))) - .subcommand(SubCommand::with_name("sort").about("Sorts a log by time")) + .subcommand(SubCommand::with_name("sort") + .about("Sorts a log by time") + .setting(AppSettings::AllowLeadingHyphen)) .subcommand(SubCommand::with_name("dedup") - .about("Removes duplicate log entries in close proximity")) + .about("Removes duplicate log entries in close proximity") + .setting(AppSettings::AllowLeadingHyphen)) .subcommand(SubCommand::with_name("merge") .about("Merges the input logs. This has to keep everything \ - in memory")) + in memory") + .setting(AppSettings::AllowLeadingHyphen)) .get_matches(); if args.is_present("notice") { @@ -223,6 +234,21 @@ pub fn die(s: &str) -> ! { process::exit(1) } +macro_rules! error { + ($code: expr, $fmt:expr) => {{ + use std::io::Write; + let err = std::io::stderr(); + let _ = writeln!(&mut err.lock(), $fmt); + std::process::exit($code); + }}; + ($code: expr, $fmt:expr, $($arg:tt)*) => {{ + use std::io::Write; + let err = std::io::stderr(); + let _ = writeln!(&mut err.lock(), $fmt, $($arg)*); + std::process::exit($code); + }}; +} + pub fn decoder(format: &str) -> Option> { match format { "energymech" | "em" => Some(Box::new(Energymech)), @@ -252,7 +278,7 @@ pub fn force_decoder(s: Option<&str>) -> Box { }; match decoder(&inf) { Some(d) => d, - None => die(&format!("The format `{}` is unknown to me", inf)), + None => error!(2, "The format `{}` is unknown to me", inf), } } @@ -263,7 +289,7 @@ pub fn force_encoder<'a>(s: Option<&str>) -> Box { }; match encoder(&outf) { Some(e) => e, - None => die(&format!("The format `{}` is unknown to me", outf)), + None => error!(2, "The format `{}` is unknown to me", outf), } } @@ -306,8 +332,8 @@ impl<'a> Environment<'a> { pub fn build_context(args: &ArgMatches) -> Context { let mut context = Context { - timezone: FixedOffset::west(args.value_of("timezone") - .and_then(|s| s.parse().ok()) + timezone: FixedOffset::west(args.value_of("time") + .and_then(|s| s.parse::().ok()) .unwrap_or(0)), override_date: args.value_of("date").and_then(|d| NaiveDate::from_str(&d).ok()), channel: args.value_of("channel").map(str::to_owned).clone(), @@ -325,7 +351,7 @@ pub fn build_context(args: &ArgMatches) -> Context { context.override_date = Some(date); } } - _n => die("Too many input files, can't infer date"), + n => error!(3, "Too many input files ({}), can't infer date", n), } } context diff --git a/ops/Cargo.toml b/ops/Cargo.toml index f4bf082..6789aa2 100644 --- a/ops/Cargo.toml +++ b/ops/Cargo.toml @@ -9,6 +9,7 @@ authors = ["Till Höppner "] [dependencies] log = "0.3.5" +chrono = "0.2.19" ilc-base = "~0.2" blist = "0.0.4" bit-set = "0.3.0" diff --git a/ops/src/lib.rs b/ops/src/lib.rs index d5aa003..2ebe51f 100644 --- a/ops/src/lib.rs +++ b/ops/src/lib.rs @@ -3,6 +3,7 @@ extern crate log; extern crate blist; extern crate bit_set; extern crate serde; +extern crate chrono; extern crate ilc_base; mod ageset; diff --git a/ops/src/stats.rs b/ops/src/stats.rs index 49f4068..7e37a80 100644 --- a/ops/src/stats.rs +++ b/ops/src/stats.rs @@ -1,15 +1,21 @@ //! Per-nick word/line statistics - -use ilc_base::{self, Context, Decode, Event}; +use ilc_base::{self, Context, Decode, Event, Time}; use ilc_base::event::Type; use std::collections::HashMap; use std::io::BufRead; +use chrono::{Datelike, NaiveDateTime, Timelike}; + use serde::ser::{MapVisitor, Serialize, Serializer}; +pub type Day = [u32; 24]; +/// Weeks start on mondays. +pub type Week = [Day; 7]; + pub struct Stats { pub freqs: HashMap, + pub week: Week, } impl Serialize for Stats { @@ -22,6 +28,7 @@ impl Serialize for Stats { where S: Serializer { try!(s.serialize_struct_elt("freqs", &self.0.freqs)); + try!(s.serialize_struct_elt("week", &self.0.week)); Ok(None) } @@ -91,11 +98,19 @@ fn strip_nick(s: &str) -> &str { /// Return all active nicks, with lines, words and words per lines counted. pub fn stats(ctx: &Context, input: &mut BufRead, decoder: &mut Decode) -> ilc_base::Result { let mut freqs: HashMap = HashMap::new(); + let mut week: Week = [[0; 24]; 7]; for e in decoder.decode(&ctx, input) { let m = try!(e); match m { - Event { ty: Type::Msg { ref from, ref content, .. }, .. } => { + Event { ty: Type::Msg { ref from, ref content, .. }, ref time, .. } => { + if let &Time::Timestamp(stamp) = time { + let date = NaiveDateTime::from_timestamp(stamp, 0); + let dow = date.weekday().num_days_from_monday() as usize; + let hour = date.hour() as usize; + week[dow][hour] += 1; + } + let nick = strip_nick(from); if freqs.contains_key(nick) { let p: &mut NickStat = freqs.get_mut(nick).unwrap(); @@ -119,5 +134,8 @@ pub fn stats(ctx: &Context, input: &mut BufRead, decoder: &mut Decode) -> ilc_ba } } - Ok(Stats { freqs: freqs }) + Ok(Stats { + freqs: freqs, + week: week, + }) } -- cgit v1.2.3