<commit_before>extern crate clap;
extern crate chrono;

use std::env;
use std::fs::DirBuilder;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::Read;
use std::io;
use std::io::Seek;
use std::io::SeekFrom;
use std::ops::Sub;
use std::ops::Add;
use std::path::PathBuf;
use std::process;
use std::str;

use clap::{Arg, App, AppSettings, SubCommand};

use chrono::DateTime;
use chrono::Timelike;
use chrono::TimeZone;
use chrono::UTC;
use chrono::date::Date;

const RECORD_LENGTH: usize = 22;

#[derive(Debug)]
#[derive(PartialEq)]
enum Action {
	PunchIn,
	PunchOut,
	Unset
}

struct Record {
	timestamp: DateTime<UTC>,
	action: Action
}

fn main() {
    match ensure_log_file_exists() {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't create punch log: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

    let args = App::new("Punch").
	    about("A simple time tracker app").
	    version("0.1").
        setting(AppSettings::ArgRequiredElseHelp).
        subcommand(SubCommand::with_name("in").about("Punch in")).
        subcommand(SubCommand::with_name("out").about("Punch out")).
        subcommand(SubCommand::with_name("card").about("Display state").
	        arg(Arg::with_name("week").long("week").short("w").help("Display summary for the last week")).
	        arg(Arg::with_name("mtd").long("mtd").short("m").help("Display summary for the month to date"))).
        get_matches();

	match args.subcommand() {
		("card", Some(specifier)) => {
			if specifier.is_present("week") {
				print_weekly_summary()
			}
			else if specifier.is_present("mtd") {
				println!("Print month-to-date summary")
			}
			else {
				print_current_state();
			}
		},
		("in", _) => {
			ensure_last_record_is_of_action(Action::PunchOut);
			write_record_to_log(chrono::UTC::now(), Action::PunchIn);
		},
		("out", _) => {
			ensure_last_record_is_of_action(Action::PunchIn);
			write_record_to_log(chrono::UTC::now(), Action::PunchOut);
		},
		_ => {
			println!("Unknown command")
		}
	}
}

fn write_record_to_log(tm: DateTime<UTC>, action: Action) {
	let action_token = match action {
		Action::PunchIn => "I",
		Action::PunchOut => "O",
		Action::Unset => "U"
	};

    let mut config_file = get_conf_file(false, true).unwrap();
    let fmt = tm.format("%FT%T");
	let formatted_timestamp = fmt.to_string();
	append_to_file(format!("{}_{}\n", formatted_timestamp, action_token).as_bytes(), &mut config_file);
}

fn print_weekly_summary() {
	let start_of_week = chrono::UTC::now().with_second(0).
		map(|ts| ts.with_minute(0).
			map(|ts| ts.with_hour(0))
		).unwrap().unwrap().unwrap();

		print_daily_durations_since(start_of_week);
}

fn print_daily_durations_since(start_time: chrono::DateTime<UTC>) {
	let mut daily_durations: Vec<DailyDuration> = vec![];
	let mut record_offset = 0;
	let mut record = empty_record();
	let mut config_file = get_conf_file(true, false).unwrap();
	let &mut current_date: &mut chrono::Date<UTC> =
		&mut chrono::UTC::now().date().add(chrono::Duration::days(1));

	let mut day_count: i64 = 0;
	let mut total_seconds_in_current_day: i64 = 0;

	// TODO need to account for duration between now and last punch-in
	if get_last_record_action() == Action::PunchIn {
		record_offset = 1
	}

	loop {
		populate_record_at_offset_from_end(&mut config_file, &mut record, record_offset);
		if record.timestamp.date() != current_date && day_count != 0 {

			daily_durations.push(DailyDuration {
					date: current_date,
					duration: chrono::Duration::seconds(total_seconds_in_current_day)
			});
			day_count += 1;
			total_seconds_in_current_day = 0;
		}

		record_offset -= 1;
	}
}

struct DailyDuration {
	date: chrono::date::Date<UTC>,
	duration: chrono::Duration
}

fn print_current_state() {
    let mut config_file = get_conf_file(true, false).unwrap();
    let mut record = empty_record();

    match populate_record_at_offset_from_end(&mut config_file, &mut record, 0) {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't read entry: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

    if record.action == Action::PunchIn {
    	let current_timestamp = chrono::UTC::now();
    	let time_punched_in = current_timestamp.sub(record.timestamp);
    	println!("Punched in since {} ({})", record.timestamp, format_duration(time_punched_in))
    }
    else {
    	let mut previous_record = empty_record();
    	match populate_record_at_offset_from_end(&mut config_file, &mut previous_record, 1) {
	    	Ok(_) => {},
	    	Err(e) => {
	    		println!("Couldn't read entry: {}.\nExiting.", e);
				process::exit(1)
	    	}
	    }

    	let delta = record.timestamp.sub(previous_record.timestamp);
    	println!("Previously punched in between {} and {} ({})",
    		previous_record.timestamp, record.timestamp, format_duration(delta))
    }
}

fn format_duration(duration: chrono::Duration) -> String {
	format!("{:02}h{:02}m", duration.num_hours(), duration.num_minutes())
}

fn get_last_record_action() -> Action {
	let mut config_file = get_conf_file(true, false).unwrap();
    let mut record = empty_record();

    match populate_record_at_offset_from_end(&mut config_file, &mut record, 0) {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't create punch log: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

	record.action
}

fn ensure_last_record_is_of_action(expected_action: Action) {

    let last_action = get_last_record_action();

    if last_action != expected_action {
    	match expected_action {
    		Action::PunchIn => {
    			println!("Already punched out, punch in first!");
				process::exit(0)
    		}
    		Action::PunchOut => {
    			println!("Already punched in, punch out first!");
    			process::exit(0)
    		}
    		Action::Unset => {
    			println!("Found unset action, log file corrupt");
    			process::exit(1)
    		}
    	}
    }
}

fn get_conf_file(read: bool, append: bool) -> io::Result<File> {
	let mut conf_file = PathBuf::new();
    conf_file.push(env::home_dir().unwrap());
    conf_file.push(".punch");
    conf_file.push("punch.log");

    OpenOptions::new().read(read).append(append).open(conf_file)
}

fn empty_record() -> Record {
	Record {
    	action: Action::Unset,
    	timestamp: chrono::UTC::now()
    }
}

fn populate_record_at_offset_from_end(config_file: &mut File, record: &mut Record, offset_from_end: u64) -> Result<(), String> {
	return seek_to_record_offset(config_file, offset_from_end).
		and_then(|_| populate_record_at_current_offset(config_file, record))
}

fn populate_record_at_current_offset(f: &mut File, record: &mut Record) -> Result<(), String> {
	let mut data = [0 as u8; RECORD_LENGTH];
	let read = f.read(&mut data);
	if read.unwrap() != RECORD_LENGTH {
		panic!("Could not read complete record of {} bytes", RECORD_LENGTH)
	}
	let (ts_data, rest) = data.split_at(19);
	let timestamp = str::from_utf8(&ts_data).unwrap();
	let parse_result = chrono::UTC.datetime_from_str(&timestamp, "%FT%T");

	let record_ts = parse_result.unwrap().with_timezone(&chrono::UTC);
	record.timestamp = record_ts;
	let action_string = str::from_utf8(&rest).unwrap();
	if action_string == "_O\n" {
		record.action = Action::PunchOut;
	}
	else if action_string == "_I\n" {
		record.action = Action::PunchIn;
	}
	else {
		return Err(format!("Could not determine action type from '{}'", action_string))
	}
	Ok(())
}

fn seek_to_record_offset(f: &mut File, record_offset: u64) -> Result<(), String> {
	let m = f.metadata().unwrap();
	let file_len = m.len();

	if file_len < RECORD_LENGTH as u64 {
		return Err(String::from("No data in log - punch in first!"))
	}

	let record_length_in_bytes = RECORD_LENGTH as u64;
	let seek_offset = file_len - ((record_offset + 1) * record_length_in_bytes);

	if f.seek(SeekFrom::Start(seek_offset)).unwrap() != seek_offset {
		return Err(format!("Could not seek to record offset {}", seek_offset))
	}
	Ok(())
}

fn append_to_file(data: &[u8], f: &mut File) {
	match f.write_all(data) {
    	Ok(_) => {},
    	Err(e) => println!("Failed to write data to log: {}", e)
    }
}

fn ensure_log_file_exists() -> io::Result<()> {
    let mut conf_dir = PathBuf::new();
    conf_dir.push(env::home_dir().unwrap());
    conf_dir.push(".punch");
    let config_path = conf_dir.as_path();

    let mut conf_file_builder = PathBuf::from(config_path);
    conf_file_builder.push("punch.log");

    let mut dir_builder = DirBuilder::new();
    dir_builder.recursive(true);

    try!(dir_builder.create(config_path));

    let conf_file = conf_file_builder.as_path();
    match OpenOptions::new().create(true).write(true).open(conf_file) {
    	Ok(_) => Ok(()),
    	Err(e) => Err(e)
    }
}<commit_msg>calculate daily summary<commit_after>extern crate clap;
extern crate chrono;

use std::env;
use std::fs::DirBuilder;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::Read;
use std::io;
use std::io::Seek;
use std::io::SeekFrom;
use std::ops::Sub;
use std::ops::Add;
use std::path::PathBuf;
use std::process;
use std::str;

use clap::{Arg, App, AppSettings, SubCommand};

use chrono::Datelike;
use chrono::DateTime;
use chrono::Timelike;
use chrono::TimeZone;
use chrono::UTC;

const RECORD_LENGTH: usize = 22;

#[derive(Debug)]
#[derive(PartialEq)]
enum Action {
	PunchIn,
	PunchOut,
	Unset
}

#[derive(Debug)]
struct Record {
	timestamp: DateTime<UTC>,
	action: Action
}

fn main() {
    match ensure_log_file_exists() {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't create punch log: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

    let args = App::new("Punch").
	    about("A simple time tracker app").
	    version("0.1").
        setting(AppSettings::ArgRequiredElseHelp).
        subcommand(SubCommand::with_name("in").about("Punch in")).
        subcommand(SubCommand::with_name("out").about("Punch out")).
        subcommand(SubCommand::with_name("card").about("Display state").
	        arg(Arg::with_name("week").long("week").short("w").help("Display summary for the last week")).
	        arg(Arg::with_name("mtd").long("mtd").short("m").help("Display summary for the month to date"))).
        get_matches();

	match args.subcommand() {
		("card", Some(specifier)) => {
			if specifier.is_present("week") {
				print_weekly_summary()
			}
			else if specifier.is_present("mtd") {
				println!("Print month-to-date summary")
			}
			else {
				print_current_state();
			}
		},
		("in", _) => {
			ensure_last_record_is_of_action(Action::PunchOut);
			write_record_to_log(chrono::UTC::now(), Action::PunchIn);
		},
		("out", _) => {
			ensure_last_record_is_of_action(Action::PunchIn);
			write_record_to_log(chrono::UTC::now(), Action::PunchOut);
		},
		_ => {
			println!("Unknown command")
		}
	}
}

fn write_record_to_log(tm: DateTime<UTC>, action: Action) {
	let action_token = match action {
		Action::PunchIn => "I",
		Action::PunchOut => "O",
		Action::Unset => "U"
	};

    let mut config_file = get_conf_file(false, true).unwrap();
    let fmt = tm.format("%FT%T");
	let formatted_timestamp = fmt.to_string();
	append_to_file(format!("{}_{}\n", formatted_timestamp, action_token).as_bytes(), &mut config_file);
}

fn print_weekly_summary() {
	let mut start_of_week = chrono::UTC::now().with_second(0).
		map(|ts| ts.with_minute(0).
			map(|ts| ts.with_hour(0))
		).unwrap().unwrap().unwrap();

	loop {
		if start_of_week.weekday() == chrono::Weekday::Mon {
			break
		}
		start_of_week = start_of_week.sub(chrono::Duration::days(1));
	}

	println!("start of week: {}", start_of_week);
		print_daily_durations_since(start_of_week);
}

fn print_daily_durations_since(start_time: chrono::DateTime<UTC>) {
	let mut daily_durations: Vec<DailyDuration> = vec![];
	let mut record_offset = 0;
	let mut record = empty_record();
	let mut config_file = get_conf_file(true, false).unwrap();
	let mut current_date: chrono::Date<UTC> =
		chrono::UTC::now().date().add(chrono::Duration::days(1));

	let mut day_count: i64 = 0;
	let mut total_seconds_in_current_day: i64 = 0;
	let mut total_seconds_in_time_range: i64 = 0;

	// TODO need to account for duration between now and last punch-in
	if get_last_record_action() == Action::PunchIn {
		record_offset = 1
	}
	let mut last_punch_out_timestamp: chrono::DateTime<UTC> = chrono::UTC::now();

	loop {
		let read_attempt = populate_record_at_offset_from_end(&mut config_file, &mut record, record_offset);
		if !read_attempt.is_ok() || record.timestamp < start_time {
			if total_seconds_in_current_day != 0 {
				daily_durations.push(DailyDuration {
						date: current_date,
						duration: chrono::Duration::seconds(total_seconds_in_current_day)
				});
			}
			break
		}
		if record.timestamp.date() != current_date && day_count != 0 {
			daily_durations.push(DailyDuration {
					date: current_date,
					duration: chrono::Duration::seconds(total_seconds_in_current_day)
			});

			total_seconds_in_current_day = 0;
		}

		if record.action == Action::PunchOut {
			last_punch_out_timestamp = record.timestamp;
		} else {
			total_seconds_in_current_day += last_punch_out_timestamp.sub(record.timestamp).num_seconds();
			total_seconds_in_time_range += last_punch_out_timestamp.sub(record.timestamp).num_seconds();
		}

		record_offset += 1;
		current_date = record.timestamp.date();
		day_count += 1;
	}

	daily_durations.reverse();

	for daily_duration in &daily_durations {
		println!("{}: {}", daily_duration.date, format_duration(daily_duration.duration));
	}
	println!("\nTotal: {}", format_duration(chrono::Duration::seconds(total_seconds_in_time_range)))
}

#[derive(Debug)]
struct DailyDuration {
	date: chrono::date::Date<UTC>,
	duration: chrono::Duration
}

fn print_current_state() {
    let mut config_file = get_conf_file(true, false).unwrap();
    let mut record = empty_record();

    match populate_record_at_offset_from_end(&mut config_file, &mut record, 0) {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't read entry: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

    if record.action == Action::PunchIn {
    	let current_timestamp = chrono::UTC::now();
    	let time_punched_in = current_timestamp.sub(record.timestamp);
    	println!("Punched in since {} ({})", record.timestamp, format_duration(time_punched_in))
    }
    else {
    	let mut previous_record = empty_record();
    	match populate_record_at_offset_from_end(&mut config_file, &mut previous_record, 1) {
	    	Ok(_) => {},
	    	Err(e) => {
	    		println!("Couldn't read entry: {}.\nExiting.", e);
				process::exit(1)
	    	}
	    }

    	let delta = record.timestamp.sub(previous_record.timestamp);
    	println!("Previously punched in between {} and {} ({})",
    		previous_record.timestamp, record.timestamp, format_duration(delta))
    }
}

fn format_duration(duration: chrono::Duration) -> String {
	format!("{:02}h{:02}m", duration.num_hours(), duration.num_minutes() % 60)
}

fn get_last_record_action() -> Action {
	let mut config_file = get_conf_file(true, false).unwrap();
    let mut record = empty_record();

    match populate_record_at_offset_from_end(&mut config_file, &mut record, 0) {
    	Ok(_) => {},
    	Err(e) => {
    		println!("Couldn't create punch log: {}.\nExiting.", e);
			process::exit(1)
    	}
    }

	record.action
}

fn ensure_last_record_is_of_action(expected_action: Action) {

    let last_action = get_last_record_action();

    if last_action != expected_action {
    	match expected_action {
    		Action::PunchIn => {
    			println!("Already punched out, punch in first!");
				process::exit(0)
    		}
    		Action::PunchOut => {
    			println!("Already punched in, punch out first!");
    			process::exit(0)
    		}
    		Action::Unset => {
    			println!("Found unset action, log file corrupt");
    			process::exit(1)
    		}
    	}
    }
}

fn get_conf_file(read: bool, append: bool) -> io::Result<File> {
	let mut conf_file = PathBuf::new();
    conf_file.push(env::home_dir().unwrap());
    conf_file.push(".punch");
    conf_file.push("punch.log");

    OpenOptions::new().read(read).append(append).open(conf_file)
}

fn empty_record() -> Record {
	Record {
    	action: Action::Unset,
    	timestamp: chrono::UTC::now()
    }
}

fn populate_record_at_offset_from_end(config_file: &mut File, record: &mut Record, offset_from_end: u64) -> Result<(), String> {
	return seek_to_record_offset(config_file, offset_from_end).
		and_then(|_| populate_record_at_current_offset(config_file, record))
}

fn populate_record_at_current_offset(f: &mut File, record: &mut Record) -> Result<(), String> {
	let mut data = [0 as u8; RECORD_LENGTH];
	let read = f.read(&mut data);
	if read.unwrap() != RECORD_LENGTH {
		panic!("Could not read complete record of {} bytes", RECORD_LENGTH)
	}
	let (ts_data, rest) = data.split_at(19);
	let timestamp = str::from_utf8(&ts_data).unwrap();
	let parse_result = chrono::UTC.datetime_from_str(&timestamp, "%FT%T");

	let record_ts = parse_result.unwrap().with_timezone(&chrono::UTC);
	record.timestamp = record_ts;
	let action_string = str::from_utf8(&rest).unwrap();
	if action_string == "_O\n" {
		record.action = Action::PunchOut;
	}
	else if action_string == "_I\n" {
		record.action = Action::PunchIn;
	}
	else {
		return Err(format!("Could not determine action type from '{}'", action_string))
	}
	Ok(())
}

fn seek_to_record_offset(f: &mut File, record_offset: u64) -> Result<(), String> {
	let m = f.metadata().unwrap();
	let file_len = m.len();

	if file_len < RECORD_LENGTH as u64 {
		return Err(String::from("No data in log - punch in first!"))
	}

	let record_length_in_bytes = RECORD_LENGTH as u64;
	let seek_offset = (file_len as i64 - ((record_offset + 1) * record_length_in_bytes) as i64) as u64;
	let seek_result = f.seek(SeekFrom::Start(seek_offset));
	if !seek_result.is_ok() {
		return Err(format!("Failed to seek: {}", seek_result.err().unwrap()))
	}
	if seek_result.unwrap() != seek_offset {
		return Err(format!("Could not seek to record offset {}", seek_offset))
	}
	Ok(())
}

fn append_to_file(data: &[u8], f: &mut File) {
	match f.write_all(data) {
    	Ok(_) => {},
    	Err(e) => println!("Failed to write data to log: {}", e)
    }
}

fn ensure_log_file_exists() -> io::Result<()> {
    let mut conf_dir = PathBuf::new();
    conf_dir.push(env::home_dir().unwrap());
    conf_dir.push(".punch");
    let config_path = conf_dir.as_path();

    let mut conf_file_builder = PathBuf::from(config_path);
    conf_file_builder.push("punch.log");

    let mut dir_builder = DirBuilder::new();
    dir_builder.recursive(true);

    try!(dir_builder.create(config_path));

    let conf_file = conf_file_builder.as_path();
    match OpenOptions::new().create(true).write(true).open(conf_file) {
    	Ok(_) => Ok(()),
    	Err(e) => Err(e)
    }
}<|endoftext|>


<commit_before>mod app;
mod error;
mod exec;
mod exit_codes;
mod filesystem;
mod filetypes;
mod filter;
mod options;
mod output;
mod regex_helper;
mod walk;

use std::env;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use std::time;

use anyhow::{anyhow, Context, Result};
use atty::Stream;
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{RegexBuilder, RegexSetBuilder};

use crate::error::print_error;
use crate::exec::CommandTemplate;
use crate::exit_codes::ExitCode;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::options::Options;
use crate::regex_helper::pattern_has_uppercase_char;

// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
#[cfg(all(not(windows), not(target_env = "musl")))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

fn run() -> Result<ExitCode> {
    let matches = app::build_app().get_matches_from(env::args_os());

    // Set the current working directory of the process
    if let Some(base_directory) = matches.value_of_os("base-directory") {
        let base_directory = Path::new(base_directory);
        if !filesystem::is_dir(base_directory) {
            return Err(anyhow!(
                "The '--base-directory' path '{}' is not a directory.",
                base_directory.to_string_lossy()
            ));
        }
        env::set_current_dir(base_directory).with_context(|| {
            format!(
                "Could not set '{}' as the current working directory.",
                base_directory.to_string_lossy()
            )
        })?;
    }

    let current_directory = Path::new(".");
    if !filesystem::is_dir(current_directory) {
        return Err(anyhow!(
            "Could not retrieve current directory (has it been deleted?)."
        ));
    }

    // Get the search pattern
    let pattern = matches
        .value_of_os("pattern")
        .map(|p| {
            p.to_str()
                .ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
        })
        .transpose()?
        .unwrap_or("");

    // Get one or more root directories to search.
    let passed_arguments = matches
        .values_of_os("path")
        .or_else(|| matches.values_of_os("search-path"));

    // Assign current directory to search vector.
    let mut dir_vec: Vec<_> = vec![current_directory.to_path_buf()];

    // Assign any valid arguments to search vector.
    if let Some(paths) = passed_arguments {
        dir_vec = vec![];
        for path in paths {
            let path_buffer = PathBuf::from(path);
            if filesystem::is_dir(&path_buffer) {
                dir_vec.push(path_buffer);
            }
            else {
                print_error(format!(
                    "Search path '{}' is not a directory.",
                    path_buffer.to_string_lossy()
                ));
            }
        }
    }

    // Check if we have no valid search paths.
    if dir_vec.is_empty() {
        return Err(anyhow!("No valid search paths given."));
    }

    if matches.is_present("absolute-path") {
        dir_vec = dir_vec
            .iter()
            .map(|path_buffer| {
                path_buffer
                    .canonicalize()
                    .and_then(|pb| filesystem::absolute_path(pb.as_path()))
                    .unwrap()
            })
            .collect();
    }

    // Detect if the user accidentally supplied a path instead of a search pattern
    if !matches.is_present("full-path")
        && pattern.contains(std::path::MAIN_SEPARATOR)
        && filesystem::is_dir(Path::new(pattern))
    {
        return Err(anyhow!(
            "The search pattern '{pattern}' contains a path-separation character ('{sep}') \
             and will not lead to any search results.\n\n\
             If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n  \
             fd . '{pattern}'\n\n\
             Instead, if you want your pattern to match the full file path, use:\n\n  \
             fd --full-path '{pattern}'",
            pattern = pattern,
            sep = std::path::MAIN_SEPARATOR,
        ));
    }

    let pattern_regex = if matches.is_present("glob") && !pattern.is_empty() {
        let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
        glob.regex().to_owned()
    } else if matches.is_present("fixed-strings") {
        // Treat pattern as literal string if '--fixed-strings' is used
        regex::escape(pattern)
    } else {
        String::from(pattern)
    };

    // The search will be case-sensitive if the command line flag is set or
    // if the pattern has an uppercase character (smart case).
    let case_sensitive = !matches.is_present("ignore-case")
        && (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));

    let interactive_terminal = atty::is(Stream::Stdout);
    let colored_output = match matches.value_of("color") {
        Some("always") => true,
        Some("never") => false,
        _ => env::var_os("NO_COLOR").is_none() && interactive_terminal,
    };

    let path_separator = matches.value_of("path-separator").map(|str| str.to_owned());

    #[cfg(windows)]
    ansi_term::enable_ansi_support().ok();

    let ls_colors = if colored_output {
        Some(LsColors::from_env().unwrap_or_default())
    } else {
        None
    };

    let command = if let Some(args) = matches.values_of("exec") {
        Some(CommandTemplate::new(args))
    } else if let Some(args) = matches.values_of("exec-batch") {
        Some(CommandTemplate::new_batch(args)?)
    } else if matches.is_present("list-details") {
        let color = matches.value_of("color").unwrap_or("auto");
        let color_arg = ["--color=", color].concat();

        #[allow(unused)]
        let gnu_ls = |command_name| {
            vec![
                command_name,
                "-l",               // long listing format
                "--human-readable", // human readable file sizes
                "--directory",      // list directories themselves, not their contents
                &color_arg,
            ]
        };

        let cmd: Vec<&str> = if cfg!(unix) {
            if !cfg!(any(
                target_os = "macos",
                target_os = "dragonfly",
                target_os = "freebsd",
                target_os = "netbsd",
                target_os = "openbsd"
            )) {
                // Assume ls is GNU ls
                gnu_ls("ls")
            } else {
                // MacOS, DragonFlyBSD, FreeBSD
                use std::process::{Command, Stdio};

                // Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
                let gnu_ls_exists = Command::new("gls")
                    .arg("--version")
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .status()
                    .is_ok();

                if gnu_ls_exists {
                    gnu_ls("gls")
                } else {
                    let mut cmd = vec![
                        "ls", // BSD version of ls
                        "-l", // long listing format
                        "-h", // '--human-readable' is not available, '-h' is
                        "-d", // '--directory' is not available, but '-d' is
                    ];

                    if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
                        // -G is not available in NetBSD's and OpenBSD's ls
                        cmd.push("-G");
                    }

                    cmd
                }
            }
        } else if cfg!(windows) {
            use std::process::{Command, Stdio};

            // Use GNU ls, if available
            let gnu_ls_exists = Command::new("ls")
                .arg("--version")
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status()
                .is_ok();

            if gnu_ls_exists {
                gnu_ls("ls")
            } else {
                return Err(anyhow!(
                    "'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
                ));
            }
        } else {
            return Err(anyhow!(
                "'fd --list-details' is not supported on this platform."
            ));
        };

        Some(CommandTemplate::new_batch(&cmd).unwrap())
    } else {
        None
    };

    let size_limits = if let Some(vs) = matches.values_of("size") {
        vs.map(|sf| {
            SizeFilter::from_string(sf)
                .ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
        })
        .collect::<Result<Vec<_>>>()?
    } else {
        vec![]
    };

    let now = time::SystemTime::now();
    let mut time_constraints: Vec<TimeFilter> = Vec::new();
    if let Some(t) = matches.value_of("changed-within") {
        if let Some(f) = TimeFilter::after(&now, t) {
            time_constraints.push(f);
        } else {
            return Err(anyhow!(
                "'{}' is not a valid date or duration. See 'fd --help'.",
                t
            ));
        }
    }
    if let Some(t) = matches.value_of("changed-before") {
        if let Some(f) = TimeFilter::before(&now, t) {
            time_constraints.push(f);
        } else {
            return Err(anyhow!(
                "'{}' is not a valid date or duration. See 'fd --help'.",
                t
            ));
        }
    }

    #[cfg(unix)]
    let owner_constraint = if let Some(s) = matches.value_of("owner") {
        OwnerFilter::from_string(s)?
    } else {
        None
    };

    let config = Options {
        case_sensitive,
        search_full_path: matches.is_present("full-path"),
        ignore_hidden: !(matches.is_present("hidden")
            || matches.occurrences_of("rg-alias-hidden-ignore") >= 2),
        read_fdignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")),
        read_vcsignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")
            || matches.is_present("no-ignore-vcs")),
        read_global_ignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")
            || matches.is_present("no-global-ignore-file")),
        follow_links: matches.is_present("follow"),
        one_file_system: matches.is_present("one-file-system"),
        null_separator: matches.is_present("null_separator"),
        max_depth: matches
            .value_of("max-depth")
            .or_else(|| matches.value_of("rg-depth"))
            .or_else(|| matches.value_of("exact-depth"))
            .and_then(|n| usize::from_str_radix(n, 10).ok()),
        min_depth: matches
            .value_of("min-depth")
            .or_else(|| matches.value_of("exact-depth"))
            .and_then(|n| usize::from_str_radix(n, 10).ok()),
        threads: std::cmp::max(
            matches
                .value_of("threads")
                .and_then(|n| usize::from_str_radix(n, 10).ok())
                .unwrap_or_else(num_cpus::get),
            1,
        ),
        max_buffer_time: matches
            .value_of("max-buffer-time")
            .and_then(|n| u64::from_str_radix(n, 10).ok())
            .map(time::Duration::from_millis),
        ls_colors,
        interactive_terminal,
        file_types: matches.values_of("file-type").map(|values| {
            let mut file_types = FileTypes::default();
            for value in values {
                match value {
                    "f" | "file" => file_types.files = true,
                    "d" | "directory" => file_types.directories = true,
                    "l" | "symlink" => file_types.symlinks = true,
                    "x" | "executable" => {
                        file_types.executables_only = true;
                        file_types.files = true;
                    }
                    "e" | "empty" => file_types.empty_only = true,
                    "s" | "socket" => file_types.sockets = true,
                    "p" | "pipe" => file_types.pipes = true,
                    _ => unreachable!(),
                }
            }

            // If only 'empty' was specified, search for both files and directories:
            if file_types.empty_only && !(file_types.files || file_types.directories) {
                file_types.files = true;
                file_types.directories = true;
            }

            file_types
        }),
        extensions: matches
            .values_of("extension")
            .map(|exts| {
                let patterns = exts
                    .map(|e| e.trim_start_matches('.'))
                    .map(|e| format!(r".\.{}$", regex::escape(e)));
                RegexSetBuilder::new(patterns)
                    .case_insensitive(true)
                    .build()
            })
            .transpose()?,
        command: command.map(Arc::new),
        exclude_patterns: matches
            .values_of("exclude")
            .map(|v| v.map(|p| String::from("!") + p).collect())
            .unwrap_or_else(|| vec![]),
        ignore_files: matches
            .values_of("ignore-file")
            .map(|vs| vs.map(PathBuf::from).collect())
            .unwrap_or_else(|| vec![]),
        size_constraints: size_limits,
        time_constraints,
        #[cfg(unix)]
        owner_constraint,
        show_filesystem_errors: matches.is_present("show-errors"),
        path_separator,
        max_results: matches
            .value_of("max-results")
            .and_then(|n| usize::from_str_radix(n, 10).ok())
            .filter(|&n| n != 0)
            .or_else(|| {
                if matches.is_present("max-one-result") {
                    Some(1)
                } else {
                    None
                }
            }),
    };

    let re = RegexBuilder::new(&pattern_regex)
        .case_insensitive(!config.case_sensitive)
        .dot_matches_new_line(true)
        .build()
        .map_err(|e| {
            anyhow!(
                "{}\n\nNote: You can use the '--fixed-strings' option to search for a \
                 literal string instead of a regular expression. Alternatively, you can \
                 also use the '--glob' option to match on a glob pattern.",
                e.to_string()
            )
        })?;

    walk::scan(&dir_vec, Arc::new(re), Arc::new(config))
}

fn main() {
    let result = run();
    match result {
        Ok(exit_code) => {
            process::exit(exit_code.into());
        }
        Err(err) => {
            eprintln!("[fd error]: {}", err);
            process::exit(ExitCode::GeneralError.into());
        }
    }
}
<commit_msg>Minor refactoring<commit_after>mod app;
mod error;
mod exec;
mod exit_codes;
mod filesystem;
mod filetypes;
mod filter;
mod options;
mod output;
mod regex_helper;
mod walk;

use std::env;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use std::time;

use anyhow::{anyhow, Context, Result};
use atty::Stream;
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{RegexBuilder, RegexSetBuilder};

use crate::error::print_error;
use crate::exec::CommandTemplate;
use crate::exit_codes::ExitCode;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::options::Options;
use crate::regex_helper::pattern_has_uppercase_char;

// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
#[cfg(all(not(windows), not(target_env = "musl")))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

fn run() -> Result<ExitCode> {
    let matches = app::build_app().get_matches_from(env::args_os());

    // Set the current working directory of the process
    if let Some(base_directory) = matches.value_of_os("base-directory") {
        let base_directory = Path::new(base_directory);
        if !filesystem::is_dir(base_directory) {
            return Err(anyhow!(
                "The '--base-directory' path '{}' is not a directory.",
                base_directory.to_string_lossy()
            ));
        }
        env::set_current_dir(base_directory).with_context(|| {
            format!(
                "Could not set '{}' as the current working directory.",
                base_directory.to_string_lossy()
            )
        })?;
    }

    let current_directory = Path::new(".");
    if !filesystem::is_dir(current_directory) {
        return Err(anyhow!(
            "Could not retrieve current directory (has it been deleted?)."
        ));
    }

    // Get the search pattern
    let pattern = matches
        .value_of_os("pattern")
        .map(|p| {
            p.to_str()
                .ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
        })
        .transpose()?
        .unwrap_or("");

    // Get one or more root directories to search.
    let passed_arguments = matches
        .values_of_os("path")
        .or_else(|| matches.values_of_os("search-path"));

    let mut search_paths = if let Some(paths) = passed_arguments {
        let mut directories = vec![];
        for path in paths {
            let path_buffer = PathBuf::from(path);
            if filesystem::is_dir(&path_buffer) {
                directories.push(path_buffer);
            } else {
                print_error(format!(
                    "Search path '{}' is not a directory.",
                    path_buffer.to_string_lossy()
                ));
            }
        }

        directories
    } else {
        vec![current_directory.to_path_buf()]
    };

    // Check if we have no valid search paths.
    if search_paths.is_empty() {
        return Err(anyhow!("No valid search paths given."));
    }

    if matches.is_present("absolute-path") {
        search_paths = search_paths
            .iter()
            .map(|path_buffer| {
                path_buffer
                    .canonicalize()
                    .and_then(|pb| filesystem::absolute_path(pb.as_path()))
                    .unwrap()
            })
            .collect();
    }

    // Detect if the user accidentally supplied a path instead of a search pattern
    if !matches.is_present("full-path")
        && pattern.contains(std::path::MAIN_SEPARATOR)
        && filesystem::is_dir(Path::new(pattern))
    {
        return Err(anyhow!(
            "The search pattern '{pattern}' contains a path-separation character ('{sep}') \
             and will not lead to any search results.\n\n\
             If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n  \
             fd . '{pattern}'\n\n\
             Instead, if you want your pattern to match the full file path, use:\n\n  \
             fd --full-path '{pattern}'",
            pattern = pattern,
            sep = std::path::MAIN_SEPARATOR,
        ));
    }

    let pattern_regex = if matches.is_present("glob") && !pattern.is_empty() {
        let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
        glob.regex().to_owned()
    } else if matches.is_present("fixed-strings") {
        // Treat pattern as literal string if '--fixed-strings' is used
        regex::escape(pattern)
    } else {
        String::from(pattern)
    };

    // The search will be case-sensitive if the command line flag is set or
    // if the pattern has an uppercase character (smart case).
    let case_sensitive = !matches.is_present("ignore-case")
        && (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));

    let interactive_terminal = atty::is(Stream::Stdout);
    let colored_output = match matches.value_of("color") {
        Some("always") => true,
        Some("never") => false,
        _ => env::var_os("NO_COLOR").is_none() && interactive_terminal,
    };

    let path_separator = matches.value_of("path-separator").map(|str| str.to_owned());

    #[cfg(windows)]
    ansi_term::enable_ansi_support().ok();

    let ls_colors = if colored_output {
        Some(LsColors::from_env().unwrap_or_default())
    } else {
        None
    };

    let command = if let Some(args) = matches.values_of("exec") {
        Some(CommandTemplate::new(args))
    } else if let Some(args) = matches.values_of("exec-batch") {
        Some(CommandTemplate::new_batch(args)?)
    } else if matches.is_present("list-details") {
        let color = matches.value_of("color").unwrap_or("auto");
        let color_arg = ["--color=", color].concat();

        #[allow(unused)]
        let gnu_ls = |command_name| {
            vec![
                command_name,
                "-l",               // long listing format
                "--human-readable", // human readable file sizes
                "--directory",      // list directories themselves, not their contents
                &color_arg,
            ]
        };

        let cmd: Vec<&str> = if cfg!(unix) {
            if !cfg!(any(
                target_os = "macos",
                target_os = "dragonfly",
                target_os = "freebsd",
                target_os = "netbsd",
                target_os = "openbsd"
            )) {
                // Assume ls is GNU ls
                gnu_ls("ls")
            } else {
                // MacOS, DragonFlyBSD, FreeBSD
                use std::process::{Command, Stdio};

                // Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
                let gnu_ls_exists = Command::new("gls")
                    .arg("--version")
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .status()
                    .is_ok();

                if gnu_ls_exists {
                    gnu_ls("gls")
                } else {
                    let mut cmd = vec![
                        "ls", // BSD version of ls
                        "-l", // long listing format
                        "-h", // '--human-readable' is not available, '-h' is
                        "-d", // '--directory' is not available, but '-d' is
                    ];

                    if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
                        // -G is not available in NetBSD's and OpenBSD's ls
                        cmd.push("-G");
                    }

                    cmd
                }
            }
        } else if cfg!(windows) {
            use std::process::{Command, Stdio};

            // Use GNU ls, if available
            let gnu_ls_exists = Command::new("ls")
                .arg("--version")
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status()
                .is_ok();

            if gnu_ls_exists {
                gnu_ls("ls")
            } else {
                return Err(anyhow!(
                    "'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
                ));
            }
        } else {
            return Err(anyhow!(
                "'fd --list-details' is not supported on this platform."
            ));
        };

        Some(CommandTemplate::new_batch(&cmd).unwrap())
    } else {
        None
    };

    let size_limits = if let Some(vs) = matches.values_of("size") {
        vs.map(|sf| {
            SizeFilter::from_string(sf)
                .ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
        })
        .collect::<Result<Vec<_>>>()?
    } else {
        vec![]
    };

    let now = time::SystemTime::now();
    let mut time_constraints: Vec<TimeFilter> = Vec::new();
    if let Some(t) = matches.value_of("changed-within") {
        if let Some(f) = TimeFilter::after(&now, t) {
            time_constraints.push(f);
        } else {
            return Err(anyhow!(
                "'{}' is not a valid date or duration. See 'fd --help'.",
                t
            ));
        }
    }
    if let Some(t) = matches.value_of("changed-before") {
        if let Some(f) = TimeFilter::before(&now, t) {
            time_constraints.push(f);
        } else {
            return Err(anyhow!(
                "'{}' is not a valid date or duration. See 'fd --help'.",
                t
            ));
        }
    }

    #[cfg(unix)]
    let owner_constraint = if let Some(s) = matches.value_of("owner") {
        OwnerFilter::from_string(s)?
    } else {
        None
    };

    let config = Options {
        case_sensitive,
        search_full_path: matches.is_present("full-path"),
        ignore_hidden: !(matches.is_present("hidden")
            || matches.occurrences_of("rg-alias-hidden-ignore") >= 2),
        read_fdignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")),
        read_vcsignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")
            || matches.is_present("no-ignore-vcs")),
        read_global_ignore: !(matches.is_present("no-ignore")
            || matches.is_present("rg-alias-hidden-ignore")
            || matches.is_present("no-global-ignore-file")),
        follow_links: matches.is_present("follow"),
        one_file_system: matches.is_present("one-file-system"),
        null_separator: matches.is_present("null_separator"),
        max_depth: matches
            .value_of("max-depth")
            .or_else(|| matches.value_of("rg-depth"))
            .or_else(|| matches.value_of("exact-depth"))
            .and_then(|n| usize::from_str_radix(n, 10).ok()),
        min_depth: matches
            .value_of("min-depth")
            .or_else(|| matches.value_of("exact-depth"))
            .and_then(|n| usize::from_str_radix(n, 10).ok()),
        threads: std::cmp::max(
            matches
                .value_of("threads")
                .and_then(|n| usize::from_str_radix(n, 10).ok())
                .unwrap_or_else(num_cpus::get),
            1,
        ),
        max_buffer_time: matches
            .value_of("max-buffer-time")
            .and_then(|n| u64::from_str_radix(n, 10).ok())
            .map(time::Duration::from_millis),
        ls_colors,
        interactive_terminal,
        file_types: matches.values_of("file-type").map(|values| {
            let mut file_types = FileTypes::default();
            for value in values {
                match value {
                    "f" | "file" => file_types.files = true,
                    "d" | "directory" => file_types.directories = true,
                    "l" | "symlink" => file_types.symlinks = true,
                    "x" | "executable" => {
                        file_types.executables_only = true;
                        file_types.files = true;
                    }
                    "e" | "empty" => file_types.empty_only = true,
                    "s" | "socket" => file_types.sockets = true,
                    "p" | "pipe" => file_types.pipes = true,
                    _ => unreachable!(),
                }
            }

            // If only 'empty' was specified, search for both files and directories:
            if file_types.empty_only && !(file_types.files || file_types.directories) {
                file_types.files = true;
                file_types.directories = true;
            }

            file_types
        }),
        extensions: matches
            .values_of("extension")
            .map(|exts| {
                let patterns = exts
                    .map(|e| e.trim_start_matches('.'))
                    .map(|e| format!(r".\.{}$", regex::escape(e)));
                RegexSetBuilder::new(patterns)
                    .case_insensitive(true)
                    .build()
            })
            .transpose()?,
        command: command.map(Arc::new),
        exclude_patterns: matches
            .values_of("exclude")
            .map(|v| v.map(|p| String::from("!") + p).collect())
            .unwrap_or_else(|| vec![]),
        ignore_files: matches
            .values_of("ignore-file")
            .map(|vs| vs.map(PathBuf::from).collect())
            .unwrap_or_else(|| vec![]),
        size_constraints: size_limits,
        time_constraints,
        #[cfg(unix)]
        owner_constraint,
        show_filesystem_errors: matches.is_present("show-errors"),
        path_separator,
        max_results: matches
            .value_of("max-results")
            .and_then(|n| usize::from_str_radix(n, 10).ok())
            .filter(|&n| n != 0)
            .or_else(|| {
                if matches.is_present("max-one-result") {
                    Some(1)
                } else {
                    None
                }
            }),
    };

    let re = RegexBuilder::new(&pattern_regex)
        .case_insensitive(!config.case_sensitive)
        .dot_matches_new_line(true)
        .build()
        .map_err(|e| {
            anyhow!(
                "{}\n\nNote: You can use the '--fixed-strings' option to search for a \
                 literal string instead of a regular expression. Alternatively, you can \
                 also use the '--glob' option to match on a glob pattern.",
                e.to_string()
            )
        })?;

    walk::scan(&search_paths, Arc::new(re), Arc::new(config))
}

fn main() {
    let result = run();
    match result {
        Ok(exit_code) => {
            process::exit(exit_code.into());
        }
        Err(err) => {
            eprintln!("[fd error]: {}", err);
            process::exit(ExitCode::GeneralError.into());
        }
    }
}
<|endoftext|>


<commit_before>extern crate regex;
extern crate readline;

use regex::Regex;
use std::collections::HashMap;
use std::io::prelude::*;
use std::iter::Iterator;
use std::string::String;

#[derive(Debug, PartialEq, Clone)]
enum Atom {
    Integer(i64),
    Float(f64),
    Symbol(String),
    Boolean(bool),
    Nil
}

type Function = Fn(&Vec<Atom>) -> AtomResult;
type FunctionMap = HashMap<String, Box<Function>>;

#[derive(Debug)]
enum Error {
    UnexpectedType,
    Parser,
    InvalidArguments,
    NotAFunction,
    NotImplemented
}

type AtomResult = Result<Atom, Error>;
type ParseResult = Result<Node, Error>;

fn nil_func(_: &Vec<Atom>) -> AtomResult {
    return Ok(Atom::Nil);
}

struct Env {
    func_map: FunctionMap,
    def_map: HashMap<String, Atom>
}

fn add(v: &Vec<Atom>) -> AtomResult {
    let mut result = 0;

    for i in v {
        if let Atom::Integer(d) =  *i {
            result += d
        } else {
            return Err(Error::UnexpectedType)
        }
    }

    return Ok(Atom::Integer(result));
}

fn sub(v: &Vec<Atom>) -> AtomResult {
    if v.len() < 1 {
        return Err(Error::InvalidArguments)
    }

    let mut result = match v[0] {
        Atom::Integer(d) => d,
        _ => return Err(Error::UnexpectedType)
    };

    for i in v.iter().skip(1) {
        match *i {
            Atom::Integer(d) => result -= d,
            _ => return Err(Error::UnexpectedType)
        }
    }

    return Ok(Atom::Integer(result));
}

fn equals(args: &Vec<Atom>) -> AtomResult {
    let v = &args[0];

    for i in args.iter().skip(1) {
        if v != i {
            return Ok(Atom::Boolean(false));
        }
    }
    Ok(Atom::Boolean(true))
}

fn default_env() -> Env {
    let mut env = Env{func_map: HashMap::new(), def_map: HashMap::new()};

    env.func_map.insert("nil".to_string(), Box::new(nil_func) as Box<Function>);
    env.func_map.insert("+".to_string(), Box::new(add) as Box<Function>);
    env.func_map.insert("-".to_string(), Box::new(sub) as Box<Function>);
    env.func_map.insert("=".to_string(), Box::new(equals) as Box<Function>);

    return env;
}

fn tokenize(line: &str) -> ParseResult {
    let r = Regex::new(r"\s+").unwrap();
    let l = line.replace("(", " ( ").replace(")", " ) ").trim().to_string();
    let iter = r.split(&l);
    return read_tokens(&mut iter.peekable());
}

fn atom(token: &str) -> Atom {
    match token {
        "true" => return Atom::Boolean(true),
        "false" => return Atom::Boolean(false),
        _ => ()
    }

    let i = token.parse::<i64>();
    match i {
        Ok(v) => Atom::Integer(v),
        _ => {
            let f = token.parse::<f64>();
            match f {
                Ok(x) => Atom::Float(x),
                _ => Atom::Symbol(token.to_string())
            }
        },
    }
}

#[derive(Debug,PartialEq)]
struct Node {
    atom: Atom,
    children: Vec<Node>
}

fn read_tokens(iter: &mut std::iter::Peekable<regex::RegexSplits>) -> ParseResult {
    let mut node = Node{ atom: Atom::Nil, children: vec![] };

    match iter.next() {
        Some("(") => {
            loop {
                match iter.peek() {
                    Some(&")") => {
                        iter.next();
                        break
                    },
                    Some(_) => {
                        let token = try!(read_tokens(iter));
                        node.children.push(token);
                    },
                    None => return Err(Error::Parser)
                }
            }
        },
        Some(")") => return Err(Error::Parser),
        Some(x) => node.atom = atom(x),
        None => return Err(Error::Parser)
    }
    return Ok(node);
}

fn define(args: Vec<Atom>, env: &mut Env) -> AtomResult {
    if args.len() != 2 {
        return Err(Error::InvalidArguments);
    }

    if let Atom::Symbol(ref key) = args[0] {
        env.def_map.insert(key.clone(), args[1].clone());
        Ok(Atom::Nil)
    } else {
        Err(Error::InvalidArguments)
    }
}

fn get(args: Vec<Atom>, env: &Env) -> AtomResult {
    if args.len() != 1 {
        return Err(Error::InvalidArguments);
    }

    if let Atom::Symbol(ref s) = args[0] {
        if let Some(a) = env.def_map.get(s) {
            Ok(a.clone())
        } else {
            Ok(Atom::Nil)
        }
    } else {
        Err(Error::InvalidArguments)
    }
}

fn eval_args(node: &Node, env: &mut Env) -> Result<Vec<Atom>, Error> {
    let mut args: Vec<Atom> = vec![];

    for i in node.children.iter().skip(1) {
        let atom = try!(eval(i, env));
        args.push(atom);
    }
    return Ok(args);
}

fn eval(node: &Node, env: &mut Env) -> AtomResult {
    if let Atom::Nil = node.atom {
        if let Atom::Symbol(ref s) = node.children[0].atom {
            match s.as_ref() {
                "define" => {
                    let args = try!(eval_args(node, env));
                    return define(args, env);
                },
                "get" => {
                    return get( try!(eval_args(node, env)), env);
                 },
                "if" => {
                    let predicate = try!(eval(&node.children[1], env));

                    let truthy = match predicate {
                        Atom::Boolean(b) => b,
                        Atom::Nil => false,
                        _ => true
                    };

                    if truthy {
                        return eval(&node.children[2], env);
                    } else if node.children.len() > 2 {
                        return eval(&node.children[3], env);
                    } else {
                        return Ok(Atom::Boolean(false))
                    }
                },
                _ => ()
            };

            let args = try!(eval_args(node, env));

            if let Some(f) = env.func_map.get(s) {
                f(&args)
            } else {
                Ok(Atom::Nil)
            }
        } else {
            Err(Error::NotAFunction)
        }
    } else {
        Ok(node.atom.clone())
    }
}

fn parse(line: &str) -> ParseResult {
    tokenize(line)
}

fn repl() {
    println!("Rust Lisp!");
    let mut env = default_env();

    loop {
        match readline::readline(">") {
            Some(s) => {
                if let Ok(p) = parse(&s) {
                    match eval(&p, &mut env) {
                        Ok(r) => println!("{:?}", r),
                        Err(e) => println!("Error in evaluation: {:?}", e)
                    }
                } else {
                    println!("Error in parsing");
                }
            },
            None => {
                println!("Exiting...");
                break;
            }
        }
    }
}

pub fn main() {
    repl();
}

#[cfg(test)]
mod test {
    use super::Node;
    use super::Atom;
    use super::atom;
    use super::parse;
    use super::default_env;
    use super::eval;

    fn teval(s: &str) -> Atom {
        eval(&parse(s).unwrap(), &mut default_env()).unwrap()
    }

    fn make_atom_node(s: &str) -> Node {
        return Node{ atom: atom(s), children: vec![]};
    }

    #[test]
    fn atom_test() {
        assert_eq!(Atom::Integer(32), atom("32"));
        assert_eq!(Atom::Symbol("symbol".to_string()), atom("symbol"));
    }

    #[test]
    fn simple_read_tokens() {
        let x = parse("(+ 1 2)").unwrap();

        assert_eq!(Atom::Nil, x.atom);
        assert_eq!(3, x.children.len());

        let xx : Vec<Node> = vec![make_atom_node("+"), make_atom_node("1"), make_atom_node("2")];

        for pair in xx.iter().zip(x.children.iter()) {
            assert_eq!(pair.0, pair.1);
        }
    }

    #[test]
    fn nested_read_tokens() {
        let x = parse("(+ 1 (* 2 2))").unwrap();

        assert_eq!( atom("+"), x.children[0].atom);
        assert_eq!( atom("1"), x.children[1].atom);
        assert_eq!( Atom::Nil, x.children[2].atom);

        assert_eq!( 3, x.children[2].children.len());
        assert_eq!( atom("*"), x.children[2].children[0].atom);
        assert_eq!( atom("2"), x.children[2].children[1].atom);
        assert_eq!( atom("2"), x.children[2].children[2].atom);
    }

    #[test]
    fn subexp_token_test() {
        let x = parse("(+ 1 (+ 2 3) 4)").unwrap();

        assert_eq!( atom("+"), x.children[0].atom);
        assert_eq!( atom("1"), x.children[1].atom);
        assert_eq!( Atom::Nil, x.children[2].atom);

        assert_eq!( 3, x.children[2].children.len());
        assert_eq!( atom("+"), x.children[2].children[0].atom);
        assert_eq!( atom("2"), x.children[2].children[1].atom);
        assert_eq!( atom("3"), x.children[2].children[2].atom);

        assert_eq!(atom("4"), x.children[3].atom);
    }

    #[test]
    fn if_special_form() {
        let x = eval(&parse("(if (= 1 1) true false)").unwrap(), &mut default_env()).unwrap();

        assert_eq!(Atom::Boolean(true), x);
    }

    #[test]
    fn if_special_form_false() {
        let x = eval(&parse("(if (= 1 2) true false)").unwrap(), &mut default_env()).unwrap();

        assert_eq!(Atom::Boolean(false), x);
    }

    #[test]
    fn if_no_else() {
        let x = teval("(if (= 1 1) true)");

        assert_eq!(Atom::Boolean(true), x);
    }

    #[test]
    #[should_panic]
    fn unmatched_bracket() {
        parse("(+ 1").unwrap();
    }

    #[test]
    #[should_panic]
    fn eof() {
         parse("").unwrap();
    }
}
<commit_msg>switch to an enum<commit_after>extern crate regex;
extern crate readline;

use regex::Regex;
use std::collections::HashMap;
use std::io::prelude::*;
use std::iter::Iterator;
use std::string::String;

#[derive(Debug, PartialEq, Clone)]
enum Atom {
    Integer(i64),
    Float(f64),
    Symbol(String),
    Boolean(bool),
    Nil
}

type Function = Fn(&Vec<Atom>) -> AtomResult;
type FunctionMap = HashMap<String, Box<Function>>;

#[derive(Debug)]
enum Error {
    UnexpectedType,
    Parser,
    InvalidArguments,
    NotAFunction,
    NotImplemented
}

type AtomResult = Result<Atom, Error>;
type ParseResult = Result<Node, Error>;

fn nil_func(_: &Vec<Atom>) -> AtomResult {
    return Ok(Atom::Nil);
}

struct Env {
    func_map: FunctionMap,
    def_map: HashMap<String, Atom>
}

fn add(v: &Vec<Atom>) -> AtomResult {
    let mut result = 0;

    for i in v {
        if let Atom::Integer(d) =  *i {
            result += d
        } else {
            return Err(Error::UnexpectedType)
        }
    }

    return Ok(Atom::Integer(result));
}

fn sub(v: &Vec<Atom>) -> AtomResult {
    if v.len() < 1 {
        return Err(Error::InvalidArguments)
    }

    let mut result = match v[0] {
        Atom::Integer(d) => d,
        _ => return Err(Error::UnexpectedType)
    };

    for i in v.iter().skip(1) {
        match *i {
            Atom::Integer(d) => result -= d,
            _ => return Err(Error::UnexpectedType)
        }
    }

    return Ok(Atom::Integer(result));
}

fn equals(args: &Vec<Atom>) -> AtomResult {
    let v = &args[0];

    for i in args.iter().skip(1) {
        if v != i {
            return Ok(Atom::Boolean(false));
        }
    }
    Ok(Atom::Boolean(true))
}

fn default_env() -> Env {
    let mut env = Env{func_map: HashMap::new(), def_map: HashMap::new()};

    env.func_map.insert("nil".to_string(), Box::new(nil_func) as Box<Function>);
    env.func_map.insert("+".to_string(), Box::new(add) as Box<Function>);
    env.func_map.insert("-".to_string(), Box::new(sub) as Box<Function>);
    env.func_map.insert("=".to_string(), Box::new(equals) as Box<Function>);

    return env;
}

fn tokenize(line: &str) -> ParseResult {
    let r = Regex::new(r"\s+").unwrap();
    let l = line.replace("(", " ( ").replace(")", " ) ").trim().to_string();
    let iter = r.split(&l);
    return read_tokens(&mut iter.peekable());
}

fn atom(token: &str) -> Atom {
    match token {
        "true" => return Atom::Boolean(true),
        "false" => return Atom::Boolean(false),
        _ => ()
    }

    let i = token.parse::<i64>();
    match i {
        Ok(v) => Atom::Integer(v),
        _ => {
            let f = token.parse::<f64>();
            match f {
                Ok(x) => Atom::Float(x),
                _ => Atom::Symbol(token.to_string())
            }
        },
    }
}

#[derive(Debug,PartialEq)]
enum Node {
    Atom(Atom),
    List(Vec<Node>)
}

fn read_tokens(iter: &mut std::iter::Peekable<regex::RegexSplits>) -> ParseResult {
    match iter.next() {
        Some("(") => {
            let mut node: Vec<Node> = vec![];

            loop {
                match iter.peek() {
                    Some(&")") => {
                        iter.next();
                        break
                    },
                    Some(_) => {
                        let token = try!(read_tokens(iter));
                        node.push(token);
                    },
                    None => return Err(Error::Parser)
                }
            }
            return Ok(Node::List(node));
        },
        Some(")") => return Err(Error::Parser),
        Some(x) => return Ok(Node::Atom(atom(x))),
        None => return Err(Error::Parser)
    }
    unreachable!();
}

fn define(args: Vec<Atom>, env: &mut Env) -> AtomResult {
    if args.len() != 2 {
        return Err(Error::InvalidArguments);
    }

    if let Atom::Symbol(ref key) = args[0] {
        env.def_map.insert(key.clone(), args[1].clone());
        Ok(Atom::Nil)
    } else {
        Err(Error::InvalidArguments)
    }
}

fn get(args: Vec<Atom>, env: &Env) -> AtomResult {
    if args.len() != 1 {
        return Err(Error::InvalidArguments);
    }

    if let Atom::Symbol(ref s) = args[0] {
        if let Some(a) = env.def_map.get(s) {
            Ok(a.clone())
        } else {
            Ok(Atom::Nil)
        }
    } else {
        Err(Error::InvalidArguments)
    }
}

fn eval_args(list: &Vec<Node>, env: &mut Env) -> Result<Vec<Atom>, Error> {
    let mut args: Vec<Atom> = vec![];

    for i in list.iter().skip(1) {
        let atom = try!(eval(i, env));
        args.push(atom);
    }
    return Ok(args);
}

fn eval(node: &Node, env: &mut Env) -> AtomResult {
    match *node {
        Node::List(ref list) => {
            match list[0] {
                Node::Atom(ref atom) => {
                    if let &Atom::Symbol(ref s) = atom {
                        match s.as_ref() {
                            "define" => {
                                let args = try!(eval_args(&list, env));
                                return define(args, env);
                            },
                            "get" => {
                                return get( try!(eval_args(&list, env)), env);
                            },
                            "if" => {
                                let predicate = try!(eval(&list[1], env));

                                let truthy = match predicate {
                                    Atom::Boolean(b) => b,
                                    Atom::Nil => false,
                                    _ => true
                                };

                                if truthy {
                                    return eval(&list[2], env);
                                } else if list.len() > 2 {
                                    return eval(&list[3], env);
                                } else {
                                    return Ok(Atom::Boolean(false))
                                }
                            },
                            _ => ()
                        };

                        let args = try!(eval_args(&list, env));

                        if let Some(f) = env.func_map.get(s) {
                            f(&args)
                        } else {
                            Ok(Atom::Nil)
                        }
                    } else {
                        Err(Error::NotAFunction)
                    }
                },
                Node::List(_) => panic!("derp")
            }
        },
        Node::Atom(ref atom) => Ok(atom.clone())
    }
}

fn parse(line: &str) -> ParseResult {
    tokenize(line)
}

fn repl() {
    println!("Rust Lisp!");
    let mut env = default_env();

    loop {
        match readline::readline(">") {
            Some(s) => {
                if let Ok(p) = parse(&s) {
                    match eval(&p, &mut env) {
                        Ok(r) => println!("{:?}", r),
                        Err(e) => println!("Error in evaluation: {:?}", e)
                    }
                } else {
                    println!("Error in parsing");
                }
            },
            None => {
                println!("Exiting...");
                break;
            }
        }
    }
}

pub fn main() {
    repl();
}

#[cfg(test)]
mod test {
    use super::Node;
    use super::Atom;
    use super::atom;
    use super::parse;
    use super::default_env;
    use super::eval;

    fn teval(s: &str) -> Atom {
        eval(&parse(s).unwrap(), &mut default_env()).unwrap()
    }

    fn make_atom_node(s: &str) -> Node {
        return Node::Atom(atom(s));
    }

    #[test]
    fn atom_test() {
        assert_eq!(Atom::Integer(32), atom("32"));
        assert_eq!(Atom::Symbol("symbol".to_string()), atom("symbol"));
    }

    #[test]
    fn simple_read_tokens() {
        // let x = parse("(+ 1 2)").unwrap();

        // let l: Vec<Node> = match x {
        //     Node::List(l) => assert_eq!(3, l.len()),
        //     _ => assert!(false)
        // };

        // let xx : Vec<Node> = vec![make_atom_node("+"), make_atom_node("1"), make_atom_node("2")];

        // for pair in xx.iter().zip(l.iter()) {
        //     assert_eq!(pair.0, pair.1);
        // }
    }

    #[test]
    fn nested_read_tokens() {
        let x = parse("(+ 1 (* 2 2))").unwrap();

        // assert_eq!( atom("+"), x.children[0].atom);
        // assert_eq!( atom("1"), x.children[1].atom);
        // assert_eq!( Atom::Nil, x.children[2].atom);

        // assert_eq!( 3, x.children[2].children.len());
        // assert_eq!( atom("*"), x.children[2].children[0].atom);
        // assert_eq!( atom("2"), x.children[2].children[1].atom);
        // assert_eq!( atom("2"), x.children[2].children[2].atom);
    }

    #[test]
    fn subexp_token_test() {
        let x = parse("(+ 1 (+ 2 3) 4)").unwrap();

        // assert_eq!( atom("+"), x.children[0].atom);
        // assert_eq!( atom("1"), x.children[1].atom);
        // assert_eq!( Atom::Nil, x.children[2].atom);

        // assert_eq!( 3, x.children[2].children.len());
        // assert_eq!( atom("+"), x.children[2].children[0].atom);
        // assert_eq!( atom("2"), x.children[2].children[1].atom);
        // assert_eq!( atom("3"), x.children[2].children[2].atom);

        // assert_eq!(atom("4"), x.children[3].atom);
    }

    #[test]
    fn if_special_form() {
        let x = eval(&parse("(if (= 1 1) true false)").unwrap(), &mut default_env()).unwrap();

        assert_eq!(Atom::Boolean(true), x);
    }

    #[test]
    fn if_special_form_false() {
        let x = eval(&parse("(if (= 1 2) true false)").unwrap(), &mut default_env()).unwrap();

        assert_eq!(Atom::Boolean(false), x);
    }

    #[test]
    fn if_no_else() {
        let x = teval("(if (= 1 1) true)");

        assert_eq!(Atom::Boolean(true), x);
    }

    #[test]
    #[should_panic]
    fn unmatched_bracket() {
        parse("(+ 1").unwrap();
    }

    #[test]
    #[should_panic]
    fn eof() {
         parse("").unwrap();
    }
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate chomp;

use std::fs::File;
use std::str;
use chomp::{Input, U8Result, parse_only};
use chomp::{count, option, or, string, take_while, take_while1, token};
use chomp::ascii::{digit};
use chomp::buffer::{Source, Stream, StreamError};

// HELPERS

fn to_i32(slice: Vec<u8>) -> i32 {
    slice.iter().fold(0, |acc, &d| (acc * 10) + ((d - ('0' as u8)) as i32))
}

fn make_quantity(sign: u8, number: &[u8]) -> String {
    let mut qty = String::new();
    if sign == b'-' {
        qty.push_str(str::from_utf8(&[sign]).unwrap());
    }
    qty.push_str(str::from_utf8(number).unwrap());
    qty.replace(",", "");
    qty
}

fn is_whitespace_char(c: u8) -> bool {
    c == b'\t' || c == b' '
}

fn is_quoted_symbol_char(c: u8) -> bool {
    c != b'\"' && c != b'\r' && c != b'\n'
}

fn is_digit_char(c: u8) -> bool {
    b'0' <= c && c <= b'9'
}

fn is_newline_char(c: u8) -> bool {
    c == b'\r' && c == b'\n'
}

fn is_unquoted_symbol_char(c: u8) -> bool {
    c != b'-' && c != b';' && c != b'\"' && !is_newline_char(c)
     && !is_digit_char(c) && !is_whitespace_char(c)
}

fn is_quantity_char(c: u8) -> bool {
    is_digit_char(c) || c == b'.' || c == b','
}

// PARSERS

fn whitespace(i: Input<u8>) -> U8Result<bool> {
    take_while(i, is_whitespace_char).map(|ws| ws.len() > 0)
}

fn mandatory_whitespace(i: Input<u8>) -> U8Result<()> {
    take_while1(i, is_whitespace_char).map(|_| ())
}

fn line_ending(i: Input<u8>) -> U8Result<()> {
    or(i,
        |i| token(i, b'\n').map(|_| ()),
        |i| string(i, b"\r\n").map(|_| ()))
}

fn year(i: Input<u8>) -> U8Result<i32> {
    count(i, 4, |i| digit(i)).map(to_i32)
}

fn month(i: Input<u8>) -> U8Result<i32> {
    count(i, 2, |i| digit(i)).map(to_i32)
}

fn day(i: Input<u8>) -> U8Result<i32> {
    count(i, 2, |i| digit(i)).map(to_i32)
}

fn date(i: Input<u8>) -> U8Result<(i32, i32, i32)> {
    parse!{i;
        let year =  year();
                    token(b'-');
        let month = month();
                    token(b'-');
        let day =   day();
        ret (year, month, day)
    }
}

fn quoted_symbol(i: Input<u8>) -> U8Result<&str> {
    parse!{i;
        token(b'\"');
        let symbol = take_while1(is_quoted_symbol_char);
        token(b'\"');
        ret str::from_utf8(symbol).unwrap()
    }
}

fn unquoted_symbol(i: Input<u8>) -> U8Result<&str> {
    take_while1(i, is_unquoted_symbol_char).map(|b| str::from_utf8(b).unwrap())
}

fn symbol(i: Input<u8>) -> U8Result<&str> {
    or(i, quoted_symbol, unquoted_symbol)
}

fn quantity(i: Input<u8>) -> U8Result<String> {
    parse!{i;
        let sign = option(|i| token(i, b'-'), b'+');
        let number = take_while1(is_quantity_char);
        ret make_quantity(sign, number)
    }
}

fn amount_symbol_then_quantity(i: Input<u8>) -> U8Result<(String, &str)> {
    parse!{i;
        let symbol = symbol();
        whitespace();
        let quantity = quantity();
        ret (quantity, symbol)
    }
}

fn amount_quantity_then_symbol(i: Input<u8>) -> U8Result<(String, &str)> {
    parse!{i;
        let quantity = quantity();
        whitespace();
        let symbol = symbol();
        ret (quantity, symbol)
    }
}

fn amount(i: Input<u8>) -> U8Result<(String, &str)> {
    or(i, amount_symbol_then_quantity, amount_quantity_then_symbol)
}

fn price(i: Input<u8>) -> U8Result<((i32, i32, i32), &str, (String, &str))> {
    parse!{i;
        token(b'P');
        mandatory_whitespace();
        let date = date();
        mandatory_whitespace();
        let symbol = symbol();
        mandatory_whitespace();
        let amount = amount();
        ret (date, symbol, amount)
    }
}

fn price_line(i: Input<u8>) -> U8Result<((i32, i32, i32), &str, (String, &str))> {
    parse!{i;
        let price = price();
        line_ending();
        ret price
    }
}

// MAIN

fn main() {
    // println!("{:?}", parse_only(mandatory_whitespace, b" "));
    // println!("{:?}", parse_only(date, b"2016-02-06"));
    // println!("{:?}", parse_only(symbol, b"\"AIM1651\""));
    // println!("{:?}", parse_only(symbol, b"$"));
    // println!("{:?}", parse_only(amount, b"$-5.82"));
    // println!("{:?}", parse_only(price, b"P 2016-02-06 \"AIM1651\" $5.82"));
    // println!("{:?}", parse_only(price_db, b"P 2016-02-06 \"AIM1651\" $5.82\r\nP 2016-02-07 \"AIM1651\" $5.85"));

    let price_db_filepath = "/Users/mark/Nexus/Documents/finances/ledger/.pricedb";
    let file = File::open(price_db_filepath).ok().expect("Failed to open file");

    let mut source = Source::new(file);
    let mut n = 0;

    loop {
        match source.parse(price_line) {
            Ok(_)                        => n += 1,
            Err(StreamError::Retry)      => {}, // Needed to refill buffer when necessary
            Err(StreamError::EndOfInput) => break,
            Err(e)                       => { panic!("{:?}", e); }
        }
    }

    println!("Parsed {} prices", n);

    // match source.parse(price_db) {
    //     Ok(prices)  => println!("Parsed {} prices", prices.len()),
    //     Err(e)      => println!("Uhm, wat? {:?}", e)
    // }
}
<commit_msg>Date struct and date tests<commit_after>#[macro_use]
extern crate chomp;

use std::fs::File;
use std::str;
use chomp::{Input, U8Result};
use chomp::{count, option, or, string, take_while, take_while1, token};
use chomp::ascii::{digit};
use chomp::buffer::{Source, Stream, StreamError};

// TYPES

#[derive(PartialEq, Debug)]
struct Date {
    year: i32,
    month: i32,
    day: i32
}

// HELPERS

fn to_i32(slice: Vec<u8>) -> i32 {
    // TODO: make "safe" -- ensure all u8's are actually "digits"
    slice.iter().fold(0, |acc, &d| (acc * 10) + ((d - ('0' as u8)) as i32))
}

fn make_quantity(sign: u8, number: &[u8]) -> String {
    let mut qty = String::new();
    if sign == b'-' {
        qty.push_str(str::from_utf8(&[sign]).unwrap());
    }
    qty.push_str(str::from_utf8(number).unwrap());
    qty.replace(",", "");
    qty
}

fn is_whitespace_char(c: u8) -> bool {
    c == b'\t' || c == b' '
}

fn is_quoted_symbol_char(c: u8) -> bool {
    c != b'\"' && c != b'\r' && c != b'\n'
}

fn is_digit_char(c: u8) -> bool {
    b'0' <= c && c <= b'9'
}

fn is_newline_char(c: u8) -> bool {
    c == b'\r' && c == b'\n'
}

fn is_unquoted_symbol_char(c: u8) -> bool {
    c != b'-' && c != b';' && c != b'\"' && !is_newline_char(c)
     && !is_digit_char(c) && !is_whitespace_char(c)
}

fn is_quantity_char(c: u8) -> bool {
    is_digit_char(c) || c == b'.' || c == b','
}

// PARSERS

fn whitespace(i: Input<u8>) -> U8Result<bool> {
    take_while(i, is_whitespace_char).map(|ws| ws.len() > 0)
}

fn mandatory_whitespace(i: Input<u8>) -> U8Result<()> {
    take_while1(i, is_whitespace_char).map(|_| ())
}

fn line_ending(i: Input<u8>) -> U8Result<()> {
    or(i,
        |i| token(i, b'\n').map(|_| ()),
        |i| string(i, b"\r\n").map(|_| ()))
}

fn year(i: Input<u8>) -> U8Result<i32> {
    count(i, 4, |i| digit(i)).map(to_i32)
}

fn month(i: Input<u8>) -> U8Result<i32> {
    count(i, 2, |i| digit(i)).map(to_i32)
}

fn day(i: Input<u8>) -> U8Result<i32> {
    count(i, 2, |i| digit(i)).map(to_i32)
}

fn date(i: Input<u8>) -> U8Result<Date> {
    // TODO: validate year when creating
    parse!{i;
        let year =  year();
                    token(b'-');
        let month = month();
                    token(b'-');
        let day =   day();

        ret Date {
            year: year,
            month: month,
            day: day
        }
    }
}

fn quoted_symbol(i: Input<u8>) -> U8Result<&str> {
    parse!{i;
        token(b'\"');
        let symbol = take_while1(is_quoted_symbol_char);
        token(b'\"');
        ret str::from_utf8(symbol).unwrap()
    }
}

fn unquoted_symbol(i: Input<u8>) -> U8Result<&str> {
    take_while1(i, is_unquoted_symbol_char).map(|b| str::from_utf8(b).unwrap())
}

fn symbol(i: Input<u8>) -> U8Result<&str> {
    or(i, quoted_symbol, unquoted_symbol)
}

fn quantity(i: Input<u8>) -> U8Result<String> {
    parse!{i;
        let sign = option(|i| token(i, b'-'), b'+');
        let number = take_while1(is_quantity_char);
        ret make_quantity(sign, number)
    }
}

fn amount_symbol_then_quantity(i: Input<u8>) -> U8Result<(String, &str)> {
    parse!{i;
        let symbol = symbol();
        whitespace();
        let quantity = quantity();
        ret (quantity, symbol)
    }
}

fn amount_quantity_then_symbol(i: Input<u8>) -> U8Result<(String, &str)> {
    parse!{i;
        let quantity = quantity();
        whitespace();
        let symbol = symbol();
        ret (quantity, symbol)
    }
}

fn amount(i: Input<u8>) -> U8Result<(String, &str)> {
    or(i, amount_symbol_then_quantity, amount_quantity_then_symbol)
}

fn price(i: Input<u8>) -> U8Result<(Date, &str, (String, &str))> {
    parse!{i;
        token(b'P');
        mandatory_whitespace();
        let date = date();
        mandatory_whitespace();
        let symbol = symbol();
        mandatory_whitespace();
        let amount = amount();
        ret (date, symbol, amount)
    }
}

fn price_line(i: Input<u8>) -> U8Result<(Date, &str, (String, &str))> {
    parse!{i;
        let price = price();
        line_ending();
        ret price
    }
}

// MAIN

fn main() {
    // println!("{:?}", parse_only(mandatory_whitespace, b" "));
    // println!("{:?}", parse_only(symbol, b"\"AIM1651\""));
    // println!("{:?}", parse_only(symbol, b"$"));
    // println!("{:?}", parse_only(amount, b"$-5.82"));
    // println!("{:?}", parse_only(price, b"P 2016-02-06 \"AIM1651\" $5.82"));
    // println!("{:?}", parse_only(price_db, b"P 2016-02-06 \"AIM1651\" $5.82\r\nP 2016-02-07 \"AIM1651\" $5.85"));

    let price_db_filepath = "/Users/mark/Nexus/Documents/finances/ledger/.pricedb";
    let file = File::open(price_db_filepath).ok().expect("Failed to open file");

    let mut source = Source::new(file);
    let mut n = 0;

    loop {
        match source.parse(price_line) {
            Ok(_)                        => n += 1,
            Err(StreamError::Retry)      => {}, // Needed to refill buffer when necessary
            Err(StreamError::EndOfInput) => break,
            Err(e)                       => { panic!("{:?}", e); }
        }
    }

    println!("Parsed {} prices", n);

    // match source.parse(price_db) {
    //     Ok(prices)  => println!("Parsed {} prices", prices.len()),
    //     Err(e)      => println!("Uhm, wat? {:?}", e)
    // }
}

#[cfg(test)]
mod tests {
    use super::{Date};
    use super::{date, day, month, year};
    use chomp::{parse_only};

    #[test]
    fn year_valid() {
        let result = parse_only(year, b"2016");
        assert_eq!(result, Ok(2016));
    }

    #[test]
    fn month_valid() {
        let result = parse_only(month, b"02");
        assert_eq!(result, Ok(2));
    }

    #[test]
    fn day_valid() {
        let result = parse_only(day, b"07");
        assert_eq!(result, Ok(7));
    }

    #[test]
    fn date_valid() {
        let result = parse_only(date, b"2016-02-07");
        assert_eq!(result, Ok(Date {
            year: 2016,
            month: 2,
            day: 7
        }));
    }
}<|endoftext|>