-//! # Introduction
-//!
//! **fstop-print-calc** calculates times relative to a base time
//! using fractions of f-stops.
//!
-//! # F-stop printing
-//!
-//! F-stop printing refers to the concept of working in fractions of
-//! an f-stop when enlarging photographic prints, rather than linear
-//! time.
-//!
-//! Instead of making a test strip of 5, 10, 15, 20, and 25 second
-//! increments, you might make a strip in +0, +1/3, +2/3, +1, and +4/3
-//! *stops* relative to a 5s base time.
-//!
-//! The primary advantages are that test strips are created in
-//! standardized units, and a print documented in terms of f-stops can
-//! be reproduced accurately with different lenses, aperatures, and
-//! sizes.
-//!
-//!
-//! # The calculator
-//!
-//! For the most basic usage, simply run the program with a base time
-//! and it will show +/- 3 stops in 1/3rd stop increments:
-//!
-//! `$ fstop-print-calc 16`
-//!
-//! Instead of calculating a range of values, you can calculate one
-//! specific value by specifying it in the format `+X/Y` or `-X/Y`.
-//! To see +5/3rds stop:
-//!
-//! `$ fstop-print-calc 16 -- +5/3`
-//!
-//! You can specify the number of stops to display in each direction
-//! with `--stops`. This will show from -5 to +5 stops from the base:
-//!
-//! `$ fstop-print-calc 16 --stops 5`
-//!
-//! You can specify the fraction of stops to use with `--fraction`.
-//! This will show +/- 3 stops in 1/4th stop steps:
-//!
-//! `$ fstop-print-calc 16 --fraction 4`
-//!
-//! You can specify `--fraction` several times, too. This shows 1/4,
-//! 1/3, and 1/2 steps combined:
-//!
-//! `$ fstop-print-calc 16 --fraction 2 --fraction 3 --fraction 4`
-//!
-//! You can output the data in CSV format:
-//!
-//! `$ fstop-print-calc 16 --csv`
-//!
-//! Or you can output an entire CSV table, with each row being offset
-//! from the next and previous row by an f-stop fraction. Generate a
-//! +/-3 stop table in 1/3rd stop increments (19x19 table), with:
-//!
-//! `$ fstop-print-calc 16 --stops 3 --csv --csv-stops 3 --csv-fraction 3`
-//!
-//!
-//! # The math
-//!
-//! Fractional stops work the same as a compounding growth equation,
-//! where multiplying the base time (b) by some growth factor (x)
-//! repeatedly (N) times doubles the base time (1 stop increase). So:
-//!
-//! ```
-//! b*x^N = 2*b
-//! => x^N = 2 [base cancels]
-//! => x = Nth_root(2) [Nth root of both sides]
-//! => x = 2^(1/N) [alternate equivalent form]
-//! ```
-//!
-//! N is the fraction of a stop, and calculating for x finds an
-//! exponential growth factor.
-//!
-//! An example for 1/3rd stop increments:
-//!
-//! * N is 3 (denominator of 1/3)
-//! * growth factor x calculated as x = 2^(1/3) = 1.25992
-//! * base b chosen to be 10 seconds
-//! * b + 1/3rd stop = 10 * 1.25992^1 = 12.599s
-//! * b + 2/3rd stop = 10 * 1.25992^2 = 15.874s
-//! * b + 3/3rd stop = 10 * 1.25992^3 = 20.000s
-//!
-//! This works for any fraction, and for positive or negative exponents.
-//!
-//!
// # License (AGPL Version 3.0)
//
// fstop-print-calc - an f-stop enlarging time calculator
use clap::Parser;
use clap_num::number_range;
-const MAX_DENOMINATOR: usize = 16;
+const MAX_DENOMINATOR: u32 = 16;
-fn frac_range(s: &str) -> Result<usize, String> {
+fn frac_range(s: &str) -> Result<u32, String> {
number_range(s, 1, MAX_DENOMINATOR)
}
/// 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<usize>,
+ fraction: Vec<u32>,
/// Number of whole stops to calculate in each direction (default: 3)
#[arg(short, long)]
- stops: Option<i32>,
+ 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 in CSV format. Single row by default, or table if csv-fraction and csv-stops specified.
#[arg(short, long)]
csv: bool,
- /// Stop fraction to use for CSV table. Only one permitted (default: 3 i.e. 1/3rd stop)
+ /// Output CSV with formulas instead of values
#[arg(long)]
- csv_fraction: Option<usize>,
+ csv_formulas: bool,
- /// Number of whole stops to output in CSV table.
+ /// Output CSV without header row and column
#[arg(long)]
- csv_stops: Option<usize>,
-}
+ csv_skip_header: bool,
+
+ /// Fractions to use for rows if generating a table (e.g. CSV)
+ #[arg(long)]
+ table_fraction: Vec<u32>,
-fn fstop_calc(base: f64, num: f64, denom: f64, sign: f64) -> f64 {
- // This is a compounding growth equation, where multiplying the
- // base time (b) by some growth factor (x) repeatedly (N) times
- // doubles the base time (1 stop increase). So:
- //
- // b*x^N = 2*b
- // => x^N = 2 [base cancels]
- // => x = Nth_root(2) [Nth root of both sides]
- // => x = 2^(1/N) [alternate equivalent form]
- //
- // N is the fraction of a stop, and calculating for x finds an
- // exponential growth factor.
- let growth: f64 = (2_f64).powf(1./denom as f64);
-
- let multiplier: f64 = (sign as i64 * num as i64 / denom as i64) as f64;
- let modulo: f64 = (num % denom) as f64;
- base*growth.powf(multiplier*denom + sign * modulo)
+ /// 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)
+ }
}
-fn fstop_offset_str(num: f64, denom: f64, sign: f64, indent: usize, modulo: bool) -> String {
- let sign_str = match sign {
- -1.0 => "-",
- _ => "+",
+// 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(),
};
- if denom == 1.0 {
- let mult: f64 = (sign as i64 * num as i64 / denom as i64) as f64;
- format!("{}{}", sign_str, mult.abs())
- }
- else {
- let num = match modulo {
- false => num,
- true => (num % denom) as f64,
- };
- format!("{}{}{}/{}", " ".repeat(indent), sign_str, num, denom)
+
+ // 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));
+ }
}
-}
-fn fstop_strings(base: f64, num: f64, denom: f64, sign: f64, indent: usize, p: usize, modulo: bool) -> (String, String) {
- let off_str = fstop_offset_str(num, denom, sign, indent, modulo);
- let val = fstop_calc(base, num, denom, sign);
- (off_str, format!("{:.p$}", val, p=p))
-}
+ // 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.")
+ });
-fn fstop_print(base: f64, num: f64, denom: f64, sign: f64, indent: usize, p: usize, modulo: bool) {
- let (offs, val) = fstop_strings(base, num, denom, sign, indent, p, modulo);
- println!("{}: {}", offs, val);
+ // 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
}
-fn print_csv_header(base: f64, stops: i32, fractions: &Vec<(u32, u32)>, p: usize) {
- // offset header (negative)
- for i in (0..stops).rev() {
- let (i, sign) = (-i as f64, -1.0);
- for (num, denom) in fractions.iter().rev() {
- let num = *num as f64 + (-i as f64 * *denom as f64);
- let (offs, _val) = fstop_strings(base, num, *denom as f64, sign, 0, p, true);
- print!("'{offs}',");
+// 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();
- // offset header (base)
- let (offs, _val) = fstop_strings(base, 0.0, 1.0, 1.0, 0, p, true);
- print!("'{offs}',");
-
- // offset header (positive)
- for i in 0..stops {
- let (i, sign) = (i as f64, 1.0);
- for (num, denom) in fractions {
- let num = *num as f64 + (i as f64 * *denom as f64);
- let (offs, _val) = fstop_strings(base, num, *denom as f64, sign, 0, p, true);
- print!("'{offs}',");
- }
- }
- println!();
+ col_steps
}
-fn print_csv_row(base: f64, stops: i32, fractions: &Vec<(u32, u32)>, p: usize) {
- // values (negative)
- for i in (0..stops).rev() {
- let (i, sign) = (-i as f64, -1.0);
- for (num, denom) in fractions.iter().rev() {
- let num = *num as f64 + (-i as f64 * *denom as f64);
- let (_offs, val) = fstop_strings(base, num, *denom as f64, sign, 0, p, true);
- print!("{val},");
+#[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,
}
}
-
- // values (base)
- let (_offs, val) = fstop_strings(base, 0.0, 1.0, 1.0, 0, p, true);
- print!("{val},");
-
- // values (positive)
- for i in 0..stops {
- let (i, sign) = (i as f64, 1.0);
- for (num, denom) in fractions {
- let num = *num as f64 + (i as f64 * *denom as f64);
- let (_offs, val) = fstop_strings(base, num, *denom as f64, sign, 0, p, true);
- print!("{val},");
+ 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)
}
}
- println!();
}
-fn print_csv_table(base: f64, stops: i32, fractions: &Vec<(u32, u32)>, csv_stops: &Option<usize>, csv_fraction: &Option<usize>, p: usize) {
- // If both stops and fraction specified, output a full table
- match (csv_stops, csv_fraction) {
- (Some(csv_stops), Some(csv_fraction)) => {
- let csv_stops = *csv_stops as i32;
- let csv_fraction = *csv_fraction as u32;
- let mut csv_fractions: Vec<(u32, u32)> = vec!();
- for num in 1..csv_fraction {
- csv_fractions.push((num as u32, csv_fraction));
- }
- csv_fractions.push((1, 1));
-
- print_csv_header(base, csv_stops, &csv_fractions, p);
-
- // calculate and print all of the negative and +0 rows
- let tmp_base = fstop_calc(base, -stops as f64 * csv_fraction as f64, csv_fraction as f64, 1.0);
- print_csv_row(tmp_base, csv_stops, &csv_fractions, p);
- for i in (0..stops).rev() {
- for j in (0..csv_fraction).rev() {
- let num = (i as f64 * csv_fraction as f64) + j as f64;
- let tmp_base = fstop_calc(base, num, csv_fraction as f64, -1.0);
- print_csv_row(tmp_base, csv_stops, &csv_fractions, p);
- }
- }
-
- // calculate and print all of the positive rows
- for i in 0..stops {
- for j in 0..csv_fraction {
- if i == 0 && j == 0 {
- continue; // don't duplicate base
- }
- let num = (i as f64 * csv_fraction as f64) + j as f64;
- let tmp_base = fstop_calc(base, num, csv_fraction as f64, 1.0);
- print_csv_row(tmp_base, csv_stops, &csv_fractions, p);
- }
- }
- let tmp_base = fstop_calc(base, stops as f64 * csv_fraction as f64, csv_fraction as f64, 1.0);
- print_csv_row(tmp_base, csv_stops, &csv_fractions, p);
- },
- _ => {
- // output a single row
- print_csv_header(base, stops, fractions, p);
- print_csv_row(base, stops, fractions, p);
- },
+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),
+ }
}
}
-fn main() {
- let cli: CalcArgs = CalcArgs::parse();
-
- // options specified at command line
- let base = cli.time;
- let stops = cli.stops.unwrap_or(3);
- let p = cli.precision.unwrap_or(1);
+// 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))
+ };
- // default to 1/3rd of a stop
- let enabled_fracs = match cli.fraction.len() {
- 0 => vec!(3),
- _ => cli.fraction,
+ let row_steps = match cli.csv {
+ true => {
+ let fractions = denominators_to_fractions(&cli.table_fraction);
+ fractions_to_steps(&fractions, cli.table_stops.unwrap_or(3))
+ },
+ _ => vec!(Fraction::new(0., 1.)),
};
- if let Some(stop) = cli.stop {
- // 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 and print result
- fstop_print(base, num, denom, sign, 0, p, false);
- return;
+ 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)
+}
- // build a list of all possible fractions
- let mut fractions: Vec<(u32, u32)> = vec!();
- for denom in &enabled_fracs {
- for num in 1..*denom {
- fractions.push((num as u32, *denom as u32));
+// 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!();
+ }
+}
- // sort fractions
- fractions.sort_by(|a, b| {
- let fa: f64 = a.0 as f64 / a.1 as f64;
- let fb: f64 = b.0 as f64 / b.1 as f64;
- 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.0 as f64 / a.1 as f64;
- let fb: f64 = b.0 as f64 / b.1 as f64;
- fa == fb
- });
-
- // always include 1/1
- fractions.push((1, 1));
-
+// 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 {
- print_csv_table(base, stops, &fractions, &cli.csv_stops, &cli.csv_fraction, p);
+ if !cli.csv_skip_header {
+ print!(",");
+ for col_frac in col_steps {
+ print!("\"{}\",", col_frac.offset_str(0, true));
+ }
+ println!();
}
- else {
- // calculate & print negative values
- for i in (0..stops).rev() {
- let (i, sign) = (-i as f64, -1.0);
- for (num, denom) in fractions.iter().rev() {
- let num = *num as f64 + (-i as f64 * *denom as f64);
- fstop_print(base, num, *denom as f64, sign, 2, p, true);
+ 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!();
+ }
+}
- // base
- println!("+0: {:.p$}", base as f64, p=p);
+// 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 main() {
+ let cli: CalcArgs = CalcArgs::parse();
- // calculate & print positive values
- for i in 0..stops {
- let (i, sign) = (i as f64, 1.0);
- for (num, denom) in &fractions {
- let num = *num as f64 + (i as f64 * *denom as f64);
- fstop_print(base, num, *denom as f64, sign, 2, p, true);
+ match &cli.stop {
+ Some(stop) => {
+ // 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 and print result
+ let result = FstopEntry::new(cli.time, sign * num, denom);
+ 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.csv_formulas {
+ true => fstop_table_formula_csv(&col_steps, &row_steps, &table, &cli),
+ _ => fstop_table_csv(&col_steps, &row_steps, &table, &cli),
+ },
}
- }
+ },
}
}