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);
}
}
}