Compare commits

...

6 Commits

Author SHA1 Message Date
Juhani Krekelä 2b449c80f5 Implement user interface 2023-05-14 04:38:20 +03:00
Juhani Krekelä e915a3f689 Implement output formatting 2023-05-14 04:23:39 +03:00
Juhani Krekelä da620a04aa Implement most of application logic 2023-05-14 03:40:21 +03:00
Juhani Krekelä 7d82e46fd7 Implement parsing 2023-05-14 03:04:47 +03:00
Juhani Krekelä 034ffed997 Order units in order of size 2023-05-14 02:18:27 +03:00
Juhani Krekelä 7170ac3240 Reorder conversions.rs such that helpers are after primary functions 2023-05-14 02:18:09 +03:00
6 changed files with 550 additions and 35 deletions

View File

@ -1,5 +1,12 @@
use crate::units::{Metric, MetricQuantity, NonMetric, NonMetricQuantity};
pub fn convert(from: NonMetricQuantity) -> MetricQuantity {
let conversion = get_conversion(from.unit);
let amount = from.amount * conversion.to.amount / conversion.from;
let unit = conversion.to.unit;
MetricQuantity { amount, unit }
}
struct Conversion {
from: f64,
to: MetricQuantity,
@ -14,14 +21,14 @@ fn get_conversion(unit: NonMetric) -> Conversion {
match unit {
// Length
NonMetric::Foot => Conversion {
from: inch_from,
to: MetricQuantity { amount: 12.0 * inch_to, unit: Metric::Metre },
},
NonMetric::Inch => Conversion {
from: inch_from,
to: MetricQuantity { amount: inch_to, unit: Metric::Metre },
},
NonMetric::Foot => Conversion {
from: inch_from,
to: MetricQuantity { amount: 12.0 * inch_to, unit: Metric::Metre },
},
NonMetric::Yard => Conversion {
from: inch_from,
to: MetricQuantity { amount: 3.0 * 12.0 * inch_to, unit: Metric::Metre },
@ -46,33 +53,12 @@ fn get_conversion(unit: NonMetric) -> Conversion {
}
}
pub fn convert(from: NonMetricQuantity) -> MetricQuantity {
let conversion = get_conversion(from.unit);
let amount = from.amount * conversion.to.amount / conversion.from;
let unit = conversion.to.unit;
MetricQuantity { amount, unit }
}
#[cfg(test)]
mod test {
use super::*;
struct Test(NonMetric, f64);
fn run_tests(tests: &[Test], unit: Metric) {
for test in tests {
let from = NonMetricQuantity {
amount: 1.0,
unit: test.0,
};
let to = MetricQuantity {
amount: test.1,
unit: unit,
};
assert_eq!(convert(from), to);
}
}
#[test]
fn length() {
let tests = [
@ -93,4 +79,18 @@ mod test {
];
run_tests(&tests, Metric::Gram);
}
fn run_tests(tests: &[Test], unit: Metric) {
for test in tests {
let from = NonMetricQuantity {
amount: 1.0,
unit: test.0,
};
let to = MetricQuantity {
amount: test.1,
unit: unit,
};
assert_eq!(convert(from), to);
}
}
}

112
src/format.rs Normal file
View File

@ -0,0 +1,112 @@
use crate::units::{Metric, MetricQuantity};
pub fn format(quantity: MetricQuantity) -> String {
let (amount, prefix) = si_prefix(quantity.amount);
let unit = abbreviation(quantity.unit);
format!("{amount} {prefix}{unit}")
}
#[derive(Clone, Copy)]
struct SiPrefix {
prefix: &'static str,
size: f64,
}
const SI_PREFIXES: [SiPrefix; 16] = [
SiPrefix { prefix: "z", size: 0.000_000_000_000_000_000_001 },
SiPrefix { prefix: "a", size: 0.000_000_000_000_000_001 },
SiPrefix { prefix: "f", size: 0.000_000_000_000_001 },
SiPrefix { prefix: "p", size: 0.000_000_000_001 },
SiPrefix { prefix: "n", size: 0.000_000_001 },
SiPrefix { prefix: "µ", size: 0.000_001 },
SiPrefix { prefix: "m", size: 0.001 },
SiPrefix { prefix: "c", size: 0.01 }, // Included for cm
SiPrefix { prefix: "", size: 1.0 },
SiPrefix { prefix: "k", size: 1_000.0 },
SiPrefix { prefix: "M", size: 1_000_000.0 },
SiPrefix { prefix: "G", size: 1_000_000_000.0 },
SiPrefix { prefix: "T", size: 1_000_000_000_000.0 },
SiPrefix { prefix: "P", size: 1_000_000_000_000_000.0 },
SiPrefix { prefix: "E", size: 1_000_000_000_000_000_000.0 },
SiPrefix { prefix: "Z", size: 1_000_000_000_000_000_000_000.0 },
// Yotta and above cannot be represented exactly as a f64
];
fn si_prefix(amount: f64) -> (f64, &'static str) {
let absolute = amount.abs();
if absolute < SI_PREFIXES[0].size {
let prefix = SI_PREFIXES[0];
return (amount / prefix.size, prefix.prefix);
}
if absolute >= SI_PREFIXES[SI_PREFIXES.len() - 1].size {
let prefix = SI_PREFIXES[SI_PREFIXES.len() - 1];
return (amount / prefix.size, prefix.prefix);
}
// Find the correct prefix SI_PREFIX[index] such that:
// SI_PREFIXES[index].size ≤ absolute < SI_PREFIXES[index + 1].size
for index in 0..SI_PREFIXES.len() {
if SI_PREFIXES[index].size <= absolute &&
absolute < SI_PREFIXES[index + 1].size {
let prefix = SI_PREFIXES[index];
return (amount / prefix.size, prefix.prefix);
}
}
unreachable!();
}
fn abbreviation(unit: Metric) -> &'static str {
match unit {
Metric::Metre => "m",
Metric::Gram => "g",
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn quantities() {
assert_eq!("1 m", &format(MetricQuantity {
amount: 1.0,
unit: Metric::Metre,
}));
assert_eq!("5 kg", &format(MetricQuantity {
amount: 5_000.0,
unit: Metric::Gram,
}));
assert_eq!("25.5 cm", &format(MetricQuantity {
amount: 0.255,
unit: Metric::Metre,
}));
}
#[test]
fn prefixes() {
assert_eq!(si_prefix(0.000_000_000_000_000_000_0005), (0.5, "z"));
assert_eq!(si_prefix(0.000_000_000_000_000_000_001), (1.0, "z"));
assert_eq!(si_prefix(0.000_000_000_000_000_000_01), (10.0, "z"));
assert_eq!(si_prefix(0.000_000_000_000_000_00_01), (100.0, "z"));
assert_eq!(si_prefix(0.000_000_000_000_000_001), (1.0, "a"));
assert_eq!(si_prefix(0.000_000_000_000_001), (1.0, "f"));
assert_eq!(si_prefix(0.000_000_000_001), (1.0, "p"));
assert_eq!(si_prefix(0.000_000_001), (1.0, "n"));
assert_eq!(si_prefix(0.000_001), (1.0, "µ"));
assert_eq!(si_prefix(0.001), (1.0, "m"));
assert_eq!(si_prefix(0.01), (1.0, "c"));
assert_eq!(si_prefix(1.0), (1.0, ""));
assert_eq!(si_prefix(1_000.0), (1.0, "k"));
assert_eq!(si_prefix(1_000_000.0), (1.0, "M"));
assert_eq!(si_prefix(1_000_000_000.0), (1.0, "G"));
assert_eq!(si_prefix(1_000_000_000_000.0), (1.0, "T"));
assert_eq!(si_prefix(1_000_000_000_000_000.0), (1.0, "P"));
assert_eq!(si_prefix(1_000_000_000_000_000_000.0), (1.0, "E"));
assert_eq!(si_prefix(10_000_000_000_000_000_000.0), (10.0, "E"));
assert_eq!(si_prefix(100_000_000_000_000_000_000.0), (100.0, "E"));
assert_eq!(si_prefix(1_000_000_000_000_000_000_000.0), (1.0, "Z"));
assert_eq!(si_prefix(2_000_000_000_000_000_000_000.0), (2.0, "Z"));
}
}

View File

@ -1,6 +1,89 @@
mod units;
mod conversions;
mod format;
mod parse;
mod units;
pub use units::{NonMetric, NonMetricQuantity};
use conversions::convert;
use format::format;
use parse::{parse, ParseError};
use units::{MetricQuantity, NonMetric};
pub use conversions::convert;
pub fn run(input: &str) -> Result<String, String> {
let non_metric = match parse(input) {
Ok(non_metric) => non_metric,
Err(ParseError::NotValidNumber(string)) => {
return Err(format!("Not a valid number: {string}"));
}
Err(ParseError::UnexpectedUnit(unit)) => {
return Err(format!("Unexpected unit: {unit}"));
}
Err(ParseError::UnknownUnit(unit)) => {
return Err(format!("Unknown unit: {unit}"));
}
Err(ParseError::ExpectedUnit) => {
return Err("Expected a unit".to_string());
}
};
if non_metric.len() == 0 {
return Err("Expected quantity or quantities to convert".to_string());
}
let metric: Vec<MetricQuantity> = non_metric.clone().into_iter().map(convert).collect();
// Make sure the results of the conversions can be summed together
// This is the case if the units after conversion are the same
let mut metric_unit = None;
for index in 0..metric.len() {
match metric_unit {
Some(metric_unit) => {
if metric[index].unit != metric_unit {
let first_unit_name = unit_to_name(non_metric[0].unit);
let unit_name = unit_to_name(non_metric[index].unit);
return Err(format!("Incompatible units: {first_unit_name}, {unit_name}"));
}
}
None => {
metric_unit = Some(metric[index].unit);
}
}
}
let amount = metric.into_iter().map(|quantity| { quantity.amount }).sum();
let quantity = MetricQuantity {
amount: amount,
unit: metric_unit.expect("we must have at least one quantity by this point"),
};
Ok(format(quantity))
}
fn unit_to_name(unit: NonMetric) -> &'static str {
match unit {
// Length
NonMetric::Inch => "inches",
NonMetric::Foot => "feet",
NonMetric::Yard => "yards",
NonMetric::Mile => "miles",
// Weight
NonMetric::Ounce => "ounces",
NonMetric::Pound => "pounds",
NonMetric::Stone => "stones",
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn errors() {
assert_eq!(run("1.0."), Err("Not a valid number: 1.0.".to_string()));
assert_eq!(run("ft"), Err("Unexpected unit: ft".to_string()));
assert_eq!(run("1 tf"), Err("Unknown unit: tf".to_string()));
assert_eq!(run("1"), Err("Expected a unit".to_string()));
assert_eq!(run(""), Err("Expected quantity or quantities to convert".to_string()));
assert_eq!(run("6 ft 1 lbs"), Err("Incompatible units: feet, pounds".to_string()));
}
}

View File

@ -1,7 +1,39 @@
use metrify::{NonMetric, NonMetricQuantity};
use metrify::convert;
use metrify::run;
use std::env;
use std::io;
use std::io::Write;
use std::process;
fn main() {
let quantity = NonMetricQuantity { amount: 6.0, unit: NonMetric::Foot };
dbg!(convert(quantity));
let args: Vec<String> = env::args().collect();
let name = args[0].clone();
let args = args[1..].join(" ");
let mut input = args;
if input.len() == 0 {
print!("> ");
match io::stdout().flush() {
Ok(_) => {}
Err(err) => {
eprintln!("{name}: Error: {err}");
process::exit(1);
}
}
match io::stdin().read_line(&mut input) {
Ok(_) => {}
Err(err) => {
eprintln!("{name}: Error: {err}");
process::exit(1);
}
}
}
match run(&input) {
Ok(str) => println!("{str}"),
Err(err) => {
eprintln!("{name}: Error: {err}");
process::exit(1);
}
}
}

288
src/parse.rs Normal file
View File

@ -0,0 +1,288 @@
use crate::units::{NonMetric, NonMetricQuantity};
enum Expect {
Number,
Unit,
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
NotValidNumber(String),
UnexpectedUnit(String),
UnknownUnit(String),
ExpectedUnit,
}
pub fn parse(input: &str) -> Result<Vec<NonMetricQuantity>, ParseError> {
let mut quantities = Vec::new();
let mut state = Expect::Number;
let mut amount = None;
for token in tokenize(input) {
match (&state, token) {
(Expect::Number, Token::Number(number)) => {
let number = match number.trim().parse() {
Ok(number) => number,
Err(_) => {
return Err(ParseError::NotValidNumber(number));
}
};
amount = Some(number);
state = Expect::Unit;
}
(Expect::Number, Token::Unit(unit)) => {
return Err(ParseError::UnexpectedUnit(unit));
}
(Expect::Unit, Token::Number(_)) => {
unreachable!("token stream can't contain two numbers in a row");
}
(Expect::Unit, Token::Unit(unit)) => {
let unit = match parse_unit(&unit) {
Some(unit) => unit,
None => {
return Err(ParseError::UnknownUnit(unit));
}
};
let quantity = NonMetricQuantity {
amount: amount.take().expect("must have read a number to be in this state"),
unit: unit,
};
quantities.push(quantity);
state = Expect::Number;
}
}
}
match state {
Expect::Number => {},
Expect::Unit => {
return Err(ParseError::ExpectedUnit);
}
}
Ok(quantities)
}
fn parse_unit(input: &str) -> Option<NonMetric> {
match input {
// Length
"inch" => Some(NonMetric::Inch),
"inches" => Some(NonMetric::Inch),
"in" => Some(NonMetric::Inch),
"\"" => Some(NonMetric::Inch),
"" => Some(NonMetric::Inch),
"foot" => Some(NonMetric::Foot),
"feet" => Some(NonMetric::Foot),
"ft" => Some(NonMetric::Foot),
"'" => Some(NonMetric::Foot),
"" => Some(NonMetric::Foot),
"yard" => Some(NonMetric::Yard),
"yards" => Some(NonMetric::Yard),
"yd" => Some(NonMetric::Yard),
"mile" => Some(NonMetric::Mile),
"miles" => Some(NonMetric::Mile),
"mi" => Some(NonMetric::Mile),
"m" => Some(NonMetric::Mile),
// Weight
"ounce" => Some(NonMetric::Ounce),
"ounces" => Some(NonMetric::Ounce),
"oz" => Some(NonMetric::Ounce),
"pound" => Some(NonMetric::Pound),
"pounds" => Some(NonMetric::Pound),
"lb" => Some(NonMetric::Pound),
"lbs" => Some(NonMetric::Pound),
"#" => Some(NonMetric::Pound),
"stone" => Some(NonMetric::Stone),
"stones" => Some(NonMetric::Stone),
"st" => Some(NonMetric::Stone),
_ => None,
}
}
#[derive(Debug, PartialEq)]
enum Token {
Number(String),
Unit(String),
}
enum TokState {
Neutral,
Number,
Unit,
}
fn tokenize(input: &str) -> Vec<Token> {
let mut tokens = Vec::new();
let mut token = String::new();
let mut state = TokState::Neutral;
for c in input.chars() {
match state {
TokState::Neutral => {
if c.is_ascii_digit() || c == '-' {
token.push(c);
state = TokState::Number;
} else if !c.is_whitespace() {
token.push(c);
state = TokState::Unit;
}
}
TokState::Number => {
if c.is_ascii_digit() ||
c.is_whitespace() ||
c == '.' {
token.push(c);
} else {
tokens.push(Token::Number(token.trim().to_string()));
state = TokState::Unit;
token = String::new();
token.push(c);
}
}
TokState::Unit => {
if c.is_ascii_digit() || c == '-' {
tokens.push(Token::Unit(token));
state = TokState::Number;
token = String::new();
token.push(c);
}
else if !c.is_whitespace() {
token.push(c);
} else {
tokens.push(Token::Unit(token));
state = TokState::Neutral;
token = String::new();
}
}
}
}
match state {
TokState::Neutral => { assert!(token.len() == 0); }
TokState::Number => { tokens.push(Token::Number(token.trim().to_string())); }
TokState::Unit => { tokens.push(Token::Unit(token)); }
}
tokens
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parsing() {
assert_eq!(parse(""), Ok(vec![]));
assert_eq!(parse("5 ft"), Ok(vec![
NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot },
]));
assert_eq!(parse("5 ft 8 in"), Ok(vec![
NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot },
NonMetricQuantity { amount: 8.0, unit: NonMetric::Inch },
]));
assert_eq!(parse("12.0."), Err(ParseError::NotValidNumber("12.0.".to_string())));
assert_eq!(parse("ft"), Err(ParseError::UnexpectedUnit("ft".to_string())));
assert_eq!(parse("5 tf"), Err(ParseError::UnknownUnit("tf".to_string())));
assert_eq!(parse("12"), Err(ParseError::ExpectedUnit));
}
#[test]
fn units() {
// Length
assert_eq!(parse_unit("inch"), Some(NonMetric::Inch));
assert_eq!(parse_unit("inches"), Some(NonMetric::Inch));
assert_eq!(parse_unit("in"), Some(NonMetric::Inch));
assert_eq!(parse_unit("\""), Some(NonMetric::Inch));
assert_eq!(parse_unit(""), Some(NonMetric::Inch));
assert_eq!(parse_unit("foot"), Some(NonMetric::Foot));
assert_eq!(parse_unit("feet"), Some(NonMetric::Foot));
assert_eq!(parse_unit("ft"), Some(NonMetric::Foot));
assert_eq!(parse_unit("'"), Some(NonMetric::Foot));
assert_eq!(parse_unit(""), Some(NonMetric::Foot));
assert_eq!(parse_unit("yard"), Some(NonMetric::Yard));
assert_eq!(parse_unit("yards"), Some(NonMetric::Yard));
assert_eq!(parse_unit("yd"), Some(NonMetric::Yard));
assert_eq!(parse_unit("mile"), Some(NonMetric::Mile));
assert_eq!(parse_unit("miles"), Some(NonMetric::Mile));
assert_eq!(parse_unit("mi"), Some(NonMetric::Mile));
assert_eq!(parse_unit("m"), Some(NonMetric::Mile));
// Weight
assert_eq!(parse_unit("ounce"), Some(NonMetric::Ounce));
assert_eq!(parse_unit("ounces"), Some(NonMetric::Ounce));
assert_eq!(parse_unit("oz"), Some(NonMetric::Ounce));
assert_eq!(parse_unit("pound"), Some(NonMetric::Pound));
assert_eq!(parse_unit("pounds"), Some(NonMetric::Pound));
assert_eq!(parse_unit("lb"), Some(NonMetric::Pound));
assert_eq!(parse_unit("lbs"), Some(NonMetric::Pound));
assert_eq!(parse_unit("#"), Some(NonMetric::Pound));
assert_eq!(parse_unit("stone"), Some(NonMetric::Stone));
assert_eq!(parse_unit("stones"), Some(NonMetric::Stone));
assert_eq!(parse_unit("st"), Some(NonMetric::Stone));
// Unknown unit
assert_eq!(parse_unit("hutenosa"), None);
}
#[test]
fn tokens() {
assert_eq!(tokenize(""), vec![]);
assert_eq!(tokenize("10"), vec![Token::Number("10".to_string())]);
assert_eq!(tokenize(" 10 "), vec![Token::Number("10".to_string())]);
assert_eq!(tokenize("10 000"), vec![Token::Number("10 000".to_string())]);
assert_eq!(tokenize("10.0.1"), vec![Token::Number("10.0.1".to_string())]);
assert_eq!(tokenize("ft"), vec![Token::Unit("ft".to_string())]);
assert_eq!(
tokenize("10 ft"),
vec![
Token::Number("10".to_string()),
Token::Unit("ft".to_string()),
]
);
assert_eq!(
tokenize("ft in"),
vec![
Token::Unit("ft".to_string()),
Token::Unit("in".to_string()),
]
);
assert_eq!(
tokenize("5 ft 7 in"),
vec![
Token::Number("5".to_string()),
Token::Unit("ft".to_string()),
Token::Number("7".to_string()),
Token::Unit("in".to_string()),
]
);
assert_eq!(
tokenize("5\"7'"),
vec![
Token::Number("5".to_string()),
Token::Unit("\"".to_string()),
Token::Number("7".to_string()),
Token::Unit("'".to_string()),
]
);
assert_eq!(
tokenize(" 2.2lbs "),
vec![
Token::Number("2.2".to_string()),
Token::Unit("lbs".to_string()),
]
);
}
}

View File

@ -7,10 +7,10 @@ pub enum Metric {
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum NonMetric {
// Length
Foot,
Inch,
Mile,
Foot,
Yard,
Mile,
// Weight
Ounce,
Pound,
@ -23,7 +23,7 @@ pub struct MetricQuantity {
pub unit: Metric,
}
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct NonMetricQuantity {
pub amount: f64,
pub unit: NonMetric,