summary history branches tags files
src/main.rs
//! **fstop-print-calc** calculates times relative to a base time
//! using fractions of f-stops.
//!
// # License (AGPL Version 3.0)
//
// fstop-print-calc - an f-stop enlarging time calculator
// Copyright (C) 2024 Trevor Bentley <fstop-print-calc@x.mrmekon.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
use clap::Parser;
use clap_num::number_range;

const MAX_DENOMINATOR: u32 = 32;

fn frac_range(s: &str) -> Result<u32, String> {
    number_range(s, 1, MAX_DENOMINATOR)
}

/// fstop-print-calc calculates exposure times relative to a base time
/// using fractions of f-stops.
#[derive(Parser, Debug, Default)]
struct CalcArgs {
    /// Starting time to calculate relative to.
    time: f64,

    /// Specific fractional stop to calculate instead of producing tables, ex: +7/3
    stop: Option<String>,

    /// Stop fraction to calculate (1-16).  Can be provided multiple times (default: 3 i.e. 1/3rd stop)
    #[arg(short, long, value_parser=frac_range)]
    fraction: Vec<u32>,

    /// Number of whole stops to calculate in each direction (default: 3)
    #[arg(short, long)]
    stops: Option<u32>,

    /// Round to this many decimal digits (default: 1)
    #[arg(short, long)]
    precision: Option<usize>,

    /// Show times relative to base instead of absolute
    #[arg(short, long)]
    relative: bool,

    /// Output table in emacs org-mode format
    #[arg(short, long)]
    org: bool,

    /// Output table in CSV format
    #[arg(short, long)]
    csv: bool,

    /// Output tables with formulas instead of values
    #[arg(long)]
    formulas: bool,

    /// Output CSV without header row and column
    #[arg(long)]
    csv_skip_header: bool,

    /// Fractions to use for rows if generating a table (e.g. CSV)
    #[arg(long)]
    table_fraction: Vec<u32>,

    /// Number of stops to use for rows if generating a table (e.g. CSV) (default: 3)
    #[arg(long)]
    table_stops: Option<u32>,
}
impl CalcArgs {
    pub fn precision(&self) -> usize {
        self.precision.unwrap_or(1)
    }
}

// Given a list of integer denominators (2, 3, 4,...), return a
// sorted list of fractions between 0 and 1 with any equivalents
// removed (1/4, 1/3, 1/2, 2/3, 3/4,...)
fn denominators_to_fractions(denominators: &Vec<u32>) -> Vec<Fraction> {
    // default to 1/3rd of a stop
    let denominators = match denominators.len() {
        0 => vec!(3),
        _ => denominators.clone(),
    };

    // build a list of all possible fractions
    let mut fractions: Vec<Fraction> = vec!();
    for denom in &denominators {
        for num in 1..*denom {
            fractions.push(Fraction::new(num as f64, *denom as f64));
        }
    }

    // sort fractions
    fractions.sort_by(|a, b| {
        let fa: f64 = a.num() / a.denom();
        let fb: f64 = b.num() / b.denom();
        fa.partial_cmp(&fb).expect("Could not compare fractions.")
    });

    // remove duplicates (ex: 1/2 == 2/4 == 3/6)
    fractions.dedup_by(|a, b| {
        let fa: f64 = a.num() / a.denom();
        let fb: f64 = b.num() / b.denom();
        fa == fb
    });

    // always include 1/1
    fractions.push(Fraction::new(1., 1.));
    fractions
}

// Given a list of fractions (ex: 1/2, 1/3) and stops (ex: 2), return
// an expanded list of fractions covering the entire span in both
// directions (ex: -2, ..., -4/3, -1, -2/3, -1/2, -1/3, 0, 1/3, 1/2,
// ..., 2)
fn fractions_to_steps(fractions: &Vec<Fraction>, stops: u32) -> Vec<Fraction> {
    // expand fractions out to the positive stops
    let mut col_steps: Vec<Fraction> = match stops {
        0 => vec!(),
        _ => fractions.clone(),
    };
    for stop in 1..stops {
        for frac in &fractions.clone() {
            col_steps.push(Fraction::new(frac.num() + (frac.denom() * stop as f64), frac.denom()));
        }
    }
    // duplicate in the negative direction around a (0,1) base point
    let col_steps: Vec<Fraction> = col_steps.clone().iter().rev().map(|x| x.inverted())
        .chain(vec!(Fraction::new(0., 1.)))
        .chain(col_steps).collect();

    col_steps
}

#[derive(Debug, Clone)]
struct Fraction {
    numerator: f64,
    denominator: f64,
    sign: f64,
}
impl Fraction {
    pub fn new(num: f64, denom: f64) -> Fraction {
        let sign = match num {
            0.0 => 1.0,
            _ => num / num.abs(),
        };
        let num = num.abs();
        Fraction {
            numerator: num,
            denominator: denom,
            sign,
        }
    }
    pub fn modulo(&self) -> f64 {
        (self.numerator % self.denominator) as f64
    }
    pub fn inverted(&self) -> Fraction {
        Fraction {
            numerator: self.num(),
            denominator: self.denom(),
            sign: -1.0 * self.sign,
        }
    }
    pub fn num(&self) -> f64 {
        self.numerator * self.sign
    }
    pub fn denom(&self) -> f64 {
        self.denominator
    }
    pub fn offset_str(&self, indent: usize, modulo: bool) -> String {
        if self.denominator == 1.0 {
            let mult: f64 = (self.sign as i64 * self.numerator as i64 / self.denominator as i64) as f64;
            format!("{:+}", mult)
        }
        else {
            let num = match modulo {
                false => self.numerator,
                true => (self.numerator % self.denominator) as f64,
            };
            format!("{}{:+}/{}", " ".repeat(indent), self.sign * num, self.denominator)
        }
    }
}

struct FstopEntry {
    base: f64,       // relative to this base value
    frac: Fraction,  // this f-stop fraction
}
impl FstopEntry {
    pub fn new(base: f64, num: f64, denom: f64) -> FstopEntry {
        FstopEntry {
            base,
            frac: Fraction::new(num, denom),
        }
    }
    pub fn multiplier(&self) -> f64 {
        (self.frac.num() as i64 / self.frac.denom() as i64) as f64
    }
    pub fn growth(&self) -> f64 {
        (2_f64).powf(1./self.frac.denom())
    }
    pub fn abs(&self) -> f64 {
        let growth = self.base * self.growth().powf(self.multiplier() * self.frac.denominator + self.frac.sign * self.frac.modulo());
        match growth.is_nan() {
            true => self.base,
            _ => self.base * self.growth().powf(self.multiplier() * self.frac.denominator + self.frac.sign * self.frac.modulo()),
        }
    }
    pub fn abs_str(&self, precision: usize) -> String {
        format!("{:.p$}", self.abs(), p=precision)
    }
    pub fn rel(&self) -> f64 {
        self.abs() - self.base
    }
    pub fn rel_str(&self, precision: usize) -> String {
        format!("{:+.p$}", self.rel(), p=precision)
    }
    pub fn offset_str(&self, indent: usize, modulo: bool) -> String {
        self.frac.offset_str(indent, modulo)
    }
    pub fn str(&self, precision: usize, relative: bool) -> String {
        match (relative, self.frac.num() == 0.0) {
            (true, false) => self.rel_str(precision),
            _ => self.abs_str(precision),
        }
    }
    pub fn formula_str(&self, cell: String, relative: bool) -> String {
        let growth = self.base * self.growth().powf(self.multiplier() * self.frac.denominator + self.frac.sign * self.frac.modulo());
        let rel_str = match relative {
            true => format!(" - {}", cell),
            _ => format!(""),
        };
        match growth.is_nan() {
            true => format!("{}{}", self.base, rel_str),
            _ => format!("={} * POWER({}, {} * {} + {} * {}){}", cell, self.growth(), self.multiplier(), self.frac.denom(), self.frac.sign, self.frac.modulo(), rel_str),
        }
    }
    pub fn org_formula_str(&self, cell: String, relative: bool) -> String {
        let growth = self.base * self.growth().powf(self.multiplier() * self.frac.denominator + self.frac.sign * self.frac.modulo());
        let rel_str = match relative {
            true => format!(" - {}", cell),
            _ => format!(""),
        };
        match growth.is_nan() {
            true => format!("{}{}", self.base, rel_str),
            _ => format!("{}*{}**({}*{}+{}*{}){}", cell, self.growth(), self.multiplier(), self.frac.denom(), self.frac.sign, self.frac.modulo(), rel_str),
        }
    }
}

// generate a table of all f-stop fractions and their calculated times
fn fstop_table(cli: &CalcArgs) -> (Vec<Fraction>, Vec<Fraction>, Vec<FstopEntry>) {
    let col_steps = {
        let fractions = denominators_to_fractions(&cli.fraction);
        fractions_to_steps(&fractions, cli.stops.unwrap_or(3))
    };

    let row_steps = match cli.csv || cli.org {
        true => {
            let fractions = denominators_to_fractions(&cli.table_fraction);
            fractions_to_steps(&fractions, cli.table_stops.unwrap_or(3))
        },
        _ => vec!(Fraction::new(0., 1.)),
    };

    let mut table: Vec<FstopEntry> = vec!();
    for row_frac in &row_steps {
        let row_base = FstopEntry::new(cli.time, row_frac.num(), row_frac.denom());
        for col_frac in &col_steps {
            let col_base = FstopEntry::new(row_base.abs(), col_frac.num(), col_frac.denom());
            table.push(col_base);
        }
    }
    (col_steps, row_steps, table)
}

// print an f-stop table in CSV format
fn fstop_table_csv(col_steps: &Vec<Fraction>, row_steps: &Vec<Fraction>, table: &Vec<FstopEntry>, cli: &CalcArgs) {
    if !cli.csv_skip_header {
        print!(",");
        for col_frac in col_steps {
            print!("\"{}\",", col_frac.offset_str(0, true));
        }
        println!();
    }
    for (row, row_frac) in row_steps.iter().enumerate() {
        if !cli.csv_skip_header {
            print!("\"{}\",", row_frac.offset_str(0, true));
        }
        for (col, _col_frac) in col_steps.iter().enumerate() {
            print!("\"{}\",", table[row*col_steps.len() + col].str(cli.precision(), cli.relative));
        }
        println!();
    }
}

// print an f-stop table in CSV format with formulas
fn fstop_table_formula_csv(col_steps: &Vec<Fraction>, row_steps: &Vec<Fraction>, table: &Vec<FstopEntry>, cli: &CalcArgs) {
    let col_char = match cli.csv_skip_header {
        true => char::from_u32(('A' as usize + (col_steps.len() / 2)) as u32).unwrap_or('A'),
        _ => char::from_u32(('A' as usize + (col_steps.len() / 2) + 1) as u32).unwrap_or('B'),
    };
    let row_idx = match cli.csv_skip_header {
        true => (row_steps.len() / 2) + 1,
        _ => (row_steps.len() / 2) + 1 + 1,
    };

    if !cli.csv_skip_header {
        print!(",");
        for col_frac in col_steps {
            print!("\"{}\",", col_frac.offset_str(0, true));
        }
        println!();
    }
    for (row, row_frac) in row_steps.iter().enumerate() {
        if !cli.csv_skip_header {
            print!("\"{}\",", row_frac.offset_str(0, true));
        }
        for (col, _col_frac) in col_steps.iter().enumerate() {
            match col == col_steps.len() / 2 {
                true => match row == row_steps.len() / 2 {
                    // middle column and middle row == base point, insert literal value
                    true => print!("\"{}\",", table[row*col_steps.len() + col].str(cli.precision(), false)),
                    false => {
                        // make a new entry relative to the row offset
                        let entry = FstopEntry::new(table[row*col_steps.len() + col].base, row_frac.num(), row_frac.denom());
                        // base everything around the center-most field
                        let base_cell = format!("{}${}", col_char, row_idx);
                        print!("\"{}\",", entry.formula_str(base_cell, false));
                    },
                },
                false => {
                    // row is 0-indexed, spreadsheets are 1-indexed
                    let base_cell = format!("${}{}", col_char, row + match cli.csv_skip_header { true => 1, _ => 2});
                    print!("\"{}\",", table[row*col_steps.len() + col].formula_str(base_cell, cli.relative));
                }
            }
        }
        println!();
    }
}

// print an f-stop table in org-mode format
fn fstop_table_org(col_steps: &Vec<Fraction>, row_steps: &Vec<Fraction>, table: &Vec<FstopEntry>, cli: &CalcArgs) {
    println!("|-");
    print!("|");
    if !cli.csv_skip_header {
        print!(" |");
        for col_frac in col_steps {
            print!(" {} |", col_frac.offset_str(0, true));
        }
        println!();
        println!("|-");
    }
    for (row, row_frac) in row_steps.iter().enumerate() {
        print!("|");
        if !cli.csv_skip_header {
            print!(" {} |", row_frac.offset_str(0, true));
        }
        for (col, _col_frac) in col_steps.iter().enumerate() {
            print!(" {} |", table[row*col_steps.len() + col].str(cli.precision(), cli.relative));
        }
        println!();
    }
    println!("|-");
}

// print an f-stop table in org-mode format
fn fstop_table_formula_org(col_steps: &Vec<Fraction>, row_steps: &Vec<Fraction>, table: &Vec<FstopEntry>, cli: &CalcArgs) {
    let col_idx = match cli.csv_skip_header { true => (col_steps.len() / 2) + 1, _ => (col_steps.len() / 2) + 2 };
    let row_idx = match cli.csv_skip_header { true => (row_steps.len() / 2) + 1, _ => (row_steps.len() / 2) + 2 };

    println!("|-");
    print!("|");
    if !cli.csv_skip_header {
        print!(" |");
        for col_frac in col_steps {
            print!(" {} |", col_frac.offset_str(0, true));
        }
        println!();
        println!("|-");
    }
    let mut tblfmt: Vec<String> = vec!();
    for (row, row_frac) in row_steps.iter().enumerate() {
        print!("|");
        if !cli.csv_skip_header {
            print!(" {} |", row_frac.offset_str(0, true));
        }
        for (col, _col_frac) in col_steps.iter().enumerate() {
            let org_row = match cli.csv_skip_header { false => row + 2, _ => row + 1};
            let org_col = match cli.csv_skip_header { false => col + 2, _ => col + 1};

            match col == col_steps.len() / 2 {
                true => match row == row_steps.len() / 2 {
                    // middle column and middle row == base point, insert literal value
                    true => print!(" {} |", table[row*col_steps.len() + col].str(cli.precision(), false)),
                    false => {
                        // make a new entry relative to the row offset
                        let entry = FstopEntry::new(table[row*col_steps.len() + col].base, row_frac.num(), row_frac.denom());
                        // base everything around the center-most field
                        let base_cell = format!("@{}${}", row_idx, col_idx);
                        print!(" |");
                        tblfmt.push(format!("@{}${}={};%.{}f", org_row, org_col, entry.org_formula_str(base_cell, false), cli.precision()));
                    },
                },
                false => {
                    let base_cell = format!("@{}${}", row + match cli.csv_skip_header { true => 1, _ => 2}, col_idx);
                    let entry = &table[row*col_steps.len() + col];
                    let fmt_str = match cli.relative {
                        true => match entry.frac.sign {
                            -1.0 => format!("%.{}f", cli.precision()),
                            _ => format!("+%.{}f", cli.precision()),
                        },
                        false => format!("%.{}f", cli.precision()),
                    };
                    print!(" |");
                    tblfmt.push(format!("@{}${}={};{}", org_row, org_col, entry.org_formula_str(base_cell, cli.relative), fmt_str));
                },
            }
        }
        println!();
    }
    println!("|-");
    println!("#+TBLFM: {}", tblfmt.join("::"));
}

// print a single-rowed f-stop table for the terminal
fn fstop_table_print_row(col_steps: &Vec<Fraction>, table: &Vec<FstopEntry>, cli: &CalcArgs) {
    for idx in 0..col_steps.len() {
        println!("{}: {}", table[idx].offset_str(2, true), table[idx].abs_str(cli.precision()));
    }
}

fn parse_stop_string(base: f64, stop: &str) -> FstopEntry {
    // parse string manually:
    let (sign_str, fraction) = stop.split_at(1);
    let sign: f64 = match sign_str {
        "+" => { 1. },
        "-" => { -1. },
        _ => panic!("Stop missing +/- sign.  Format must be +X/Y or -X/Y."),
    };
    let slash_offs = fraction.find("/").expect("Stop missing fractional slash.  Format must be +X/Y or -X/Y.");
    let (num, denom) = fraction.split_at(slash_offs);
    let denom: &str = &denom[1..];
    let _ = frac_range(denom).expect(&format!("Stop denominator not in range 1-{}.  Format must be +X/Y or -X/Y.", MAX_DENOMINATOR));
    let num: f64 = num.parse::<u32>().expect("Stop numerator not an integer.  Format must be +X/Y or -X/Y.") as f64;
    let denom: f64 = denom.parse::<u32>().expect("Stop numerator not an integer.  Format must be +X/Y or -X/Y.") as f64;

    // calculate
    FstopEntry::new(base, sign * num, denom)
}

fn main() {
    let cli: CalcArgs = CalcArgs::parse();

    match &cli.stop {
        Some(stop) => {
            let result = parse_stop_string(cli.time, stop);
            println!("{}: {}", result.offset_str(0, false), result.str(cli.precision(), cli.relative))
        },
        _ => {
            let (col_steps, row_steps, table) = fstop_table(&cli);
            match row_steps.len() {
                1 => fstop_table_print_row(&col_steps, &table, &cli),
                _ => match cli.formulas {
                    true => match cli.org {
                        true => fstop_table_formula_org(&col_steps, &row_steps, &table, &cli),
                        _ => fstop_table_formula_csv(&col_steps, &row_steps, &table, &cli),
                    },
                    _ => match cli.org {
                        true => fstop_table_org(&col_steps, &row_steps, &table, &cli),
                        _ => fstop_table_csv(&col_steps, &row_steps, &table, &cli),
                    },
                },
            }
        },
    }
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    macro_rules! assert_almost {
        ($x:expr, $y:expr) => {
            if ($x - $y).abs() > ($x * 0.0005).abs() {
                panic!("assertion failed: left nearly right\n - left: {}\n - right: {}\n - diff: {} > {}",
                       $x, $y,
                       ($x - $y).abs(),
                       ($x * 0.0005).abs());
            }
        }
    }

    #[test]
    fn test_stops_16() {
        let b16_p2_3 = parse_stop_string(16.0, "+2/3");
        assert_eq!(b16_p2_3.base, 16.0);
        assert_eq!(b16_p2_3.frac.numerator, 2.0);
        assert_eq!(b16_p2_3.frac.num(), 2.0);
        assert_eq!(b16_p2_3.frac.denominator, 3.0);
        assert_eq!(b16_p2_3.frac.sign, 1.0);
        assert_almost!(b16_p2_3.abs(), 25.4);

        let b16_n2_3 = parse_stop_string(16.0, "-2/3");
        assert_eq!(b16_n2_3.base, 16.0);
        assert_eq!(b16_n2_3.frac.numerator, 2.0);
        assert_eq!(b16_n2_3.frac.num(), -2.0);
        assert_eq!(b16_n2_3.frac.denominator, 3.0);
        assert_eq!(b16_n2_3.frac.sign, -1.0);
        assert_almost!(b16_n2_3.abs(), 10.08);

        let b16_p1_4 = parse_stop_string(16.0, "+1/4");
        assert_eq!(b16_p1_4.base, 16.0);
        assert_eq!(b16_p1_4.frac.numerator, 1.0);
        assert_eq!(b16_p1_4.frac.num(), 1.0);
        assert_eq!(b16_p1_4.frac.denominator, 4.0);
        assert_eq!(b16_p1_4.frac.sign, 1.0);
        assert_almost!(b16_p1_4.abs(), 19.03);

        let b16_n1_4 = parse_stop_string(16.0, "-1/4");
        assert_eq!(b16_n1_4.base, 16.0);
        assert_eq!(b16_n1_4.frac.numerator, 1.0);
        assert_eq!(b16_n1_4.frac.num(), -1.0);
        assert_eq!(b16_n1_4.frac.denominator, 4.0);
        assert_eq!(b16_n1_4.frac.sign, -1.0);
        assert_almost!(b16_n1_4.abs(), 13.45);

        let b16_p3_3 = parse_stop_string(16.0, "+3/3");
        assert_eq!(b16_p3_3.base, 16.0);
        assert_eq!(b16_p3_3.frac.numerator, 3.0);
        assert_eq!(b16_p3_3.frac.num(), 3.0);
        assert_eq!(b16_p3_3.frac.denominator, 3.0);
        assert_eq!(b16_p3_3.frac.sign, 1.0);
        assert_almost!(b16_p3_3.abs(), 32.0);

        let b16_n3_3 = parse_stop_string(16.0, "-3/3");
        assert_eq!(b16_n3_3.base, 16.0);
        assert_eq!(b16_n3_3.frac.numerator, 3.0);
        assert_eq!(b16_n3_3.frac.num(), -3.0);
        assert_eq!(b16_n3_3.frac.denominator, 3.0);
        assert_eq!(b16_n3_3.frac.sign, -1.0);
        assert_almost!(b16_n3_3.abs(), 8.0);

        let b16_p5_4 = parse_stop_string(16.0, "+5/4");
        assert_eq!(b16_p5_4.base, 16.0);
        assert_eq!(b16_p5_4.frac.numerator, 5.0);
        assert_eq!(b16_p5_4.frac.num(), 5.0);
        assert_eq!(b16_p5_4.frac.denominator, 4.0);
        assert_eq!(b16_p5_4.frac.sign, 1.0);
        assert_almost!(b16_p5_4.abs(), 38.05);

        let b16_n5_4 = parse_stop_string(16.0, "-5/4");
        assert_eq!(b16_n5_4.base, 16.0);
        assert_eq!(b16_n5_4.frac.numerator, 5.0);
        assert_eq!(b16_n5_4.frac.num(), -5.0);
        assert_eq!(b16_n5_4.frac.denominator, 4.0);
        assert_eq!(b16_n5_4.frac.sign, -1.0);
        assert_almost!(b16_n5_4.abs(), 6.73);
    }

    #[test]
    fn test_table_16_3_x2() {
        let mut cli: CalcArgs = Default::default();
        cli.time = 16.0;
        cli.stops = Some(2);
        cli.fraction = vec!(3);
        let (col_steps, row_steps, table) = fstop_table(&cli);

        assert_eq!(col_steps.len(), 13);
        let expected = vec!(
            Fraction::new(-2., 1.),
            Fraction::new(-5., 3.),
            Fraction::new(-4., 3.),
            Fraction::new(-1., 1.),
            Fraction::new(-2., 3.),
            Fraction::new(-1., 3.),
            Fraction::new(0., 1.),
            Fraction::new(1., 3.),
            Fraction::new(2., 3.),
            Fraction::new(1., 1.),
            Fraction::new(4., 3.),
            Fraction::new(5., 3.),
            Fraction::new(2., 1.),
        );
        for (idx, step) in col_steps.iter().enumerate() {
            assert_eq!(step.num(), expected[idx].num());
            assert_eq!(step.denom(), expected[idx].denom());
        }

        assert_eq!(row_steps.len(), 1);

        let expected = vec!(
            4.0,
            5.04,
            6.35,
            8.0,
            10.08,
            12.70,
            16.0,
            20.16,
            25.40,
            32.0,
            40.31,
            50.80,
            64.0,
            );
        for (idx, step) in table.iter().enumerate() {
            assert_almost!(step.abs(), expected[idx]);
            assert_almost!(step.rel(), expected[idx] - 16.0);
        }
    }

    #[test]
    fn test_table_16_4_x2() {
        let mut cli: CalcArgs = Default::default();
        cli.time = 16.0;
        cli.stops = Some(2);
        cli.fraction = vec!(4);
        let (col_steps, row_steps, table) = fstop_table(&cli);

        assert_eq!(col_steps.len(), 17);
        let expected = vec!(
            Fraction::new(-2., 1.),
            Fraction::new(-7., 4.),
            Fraction::new(-6., 4.),
            Fraction::new(-5., 4.),
            Fraction::new(-1., 1.),
            Fraction::new(-3., 4.),
            Fraction::new(-2., 4.),
            Fraction::new(-1., 4.),
            Fraction::new(0., 1.),
            Fraction::new(1., 4.),
            Fraction::new(2., 4.),
            Fraction::new(3., 4.),
            Fraction::new(1., 1.),
            Fraction::new(5., 4.),
            Fraction::new(6., 4.),
            Fraction::new(7., 4.),
            Fraction::new(2., 1.),
        );
        for (idx, step) in col_steps.iter().enumerate() {
            assert_eq!(step.num(), expected[idx].num());
            assert_eq!(step.denom(), expected[idx].denom());
        }

        assert_eq!(row_steps.len(), 1);

        let expected = vec!(
            4.0,
            4.756,
            5.656,
            6.73,
            8.0,
            9.513,
            11.314,
            13.454,
            16.0,
            19.027,
            22.627,
            26.909,
            32.0,
            38.05,
            45.254,
            53.817,
            64.0,
            );
        for (idx, step) in table.iter().enumerate() {
            assert_almost!(step.abs(), expected[idx]);
            assert_almost!(step.rel(), expected[idx] - 16.0);
        }
    }

    #[test]
    fn test_table_16_3_4_x2() {
        let mut cli: CalcArgs = Default::default();
        cli.time = 16.0;
        cli.stops = Some(2);
        cli.fraction = vec!(3, 4);
        let (col_steps, row_steps, table) = fstop_table(&cli);

        assert_eq!(col_steps.len(), 25);
        let expected = vec!(
            Fraction::new(-2., 1.),
            Fraction::new(-7., 4.),
            Fraction::new(-5., 3.),
            Fraction::new(-6., 4.),
            Fraction::new(-4., 3.),
            Fraction::new(-5., 4.),
            Fraction::new(-1., 1.),
            Fraction::new(-3., 4.),
            Fraction::new(-2., 3.),
            Fraction::new(-2., 4.),
            Fraction::new(-1., 3.),
            Fraction::new(-1., 4.),
            Fraction::new(0., 1.),
            Fraction::new(1., 4.),
            Fraction::new(1., 3.),
            Fraction::new(2., 4.),
            Fraction::new(2., 3.),
            Fraction::new(3., 4.),
            Fraction::new(1., 1.),
            Fraction::new(5., 4.),
            Fraction::new(4., 3.),
            Fraction::new(6., 4.),
            Fraction::new(5., 3.),
            Fraction::new(7., 4.),
            Fraction::new(2., 1.),
        );
        for (idx, step) in col_steps.iter().enumerate() {
            assert_eq!(step.num(), expected[idx].num());
            assert_eq!(step.denom(), expected[idx].denom());
        }

        assert_eq!(row_steps.len(), 1);

        let expected = vec!(
            4.0,
            4.756,
            5.04,
            5.656,
            6.35,
            6.73,
            8.0,
            9.513,
            10.08,
            11.314,
            12.70,
            13.454,
            16.0,
            19.027,
            20.159,
            22.627,
            25.40,
            26.909,
            32.0,
            38.05,
            40.317,
            45.254,
            50.8,
            53.817,
            64.0,
            );
        for (idx, step) in table.iter().enumerate() {
            assert_almost!(step.abs(), expected[idx]);
            assert_almost!(step.rel(), expected[idx] - 16.0);
        }
    }
}