Fractions
Exact fractions for recipes, carpentry and school maths: "1 1/2", "3/8" and "12.5%" read and calculated without rounding, expressions worked out, decimals turned into simple fractions, measures rounded to 1/16 inch, and recipes scaled.
Version 1.0.0 · 7 functions · 0 crates
Functions
What use fractions.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use frac_read, frac_calc from fractions (Quillo 0.6).
frac_read(value: any) → anyor an errorA fraction read: "3/4", "1 1/2", "0.125", "12.5%", "2⅜", a number, or { num, den }. Gives { num, den, text: "3/2", mixed: "1 1/2", decimal }, always in lowest terms.
frac_calc(a: any, op: str, b: any) → anyor an errora plus, minus, times or divided by b (op "+", "-", "*", "/"), exactly.
frac_compare(a: any, b: any) → intor an error-1, 0 or 1 as a is smaller than, equal to or larger than b, exactly (1/3 and 2/6 are equal).
frac_eval(expression: str) → anyor an errorAn expression worked out exactly: "1/2 + 3/4 * (2/3 - 1/6)", with + - * / ^ and brackets; a / between plain numbers is division, so write mixed numbers as (1 + 1/2).
frac_from_decimal(x: float, max_den: int) → anyor an errorThe simplest fraction within max_den of a decimal (continued fractions): 0.333 is 1/3, 3.14159 with 1000 is 355/113. Gives it with its error.
frac_round(x: float, den: int, unit: str) → stror an errorA measure rounded to the nearest 1/den (16 for inches): 3.3 gives "3 5/16". With unit added to the text ("3 5/16 in").
frac_scale_recipe(items: list, factor: any) → listor an errorA recipe's quantities scaled by a factor ("3/2", 2, "0.5"): each item { qty: "1 1/2", unit, item } (or text like "3/4 cup sugar"), quantities kept as kitchen fractions.
Get it
$ quillo install fractions # fractions.qwer into this folder$ quillo install fractions --example # and its example, fractions.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/fractions.qwer$ curl -O https://quillolang.org/plugins/files/fractions.qweThen, in your program, all of it or only what you call:
use fractions.qweruse frac_read, frac_calc from fractions // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A woodworking and baking site: measures read as people write them ("1 1/2", "3/8", "⅜"),// summed exactly, decimals turned back into fractions, cuts rounded to the nearest 1/16 inch,// and a recipe scaled for more guests.//// quillo run fractions.qwe then open http://localhost:8080/workshop and http://localhost:8080/recipeuse "fractions.qwer"route GET /workshop = { board: frac_read("2 3/8")?, sum: frac_calc("1 1/2", "+", "⅜")?.mixed, kerf: frac_eval("(96 - 3 * 1/8) / 4")?.mixed, pi: frac_from_decimal(3.14159265, 1000)?.text, third: frac_from_decimal(0.333, 10)?.text, cut: frac_round(23.4567, 16, "in")?, equal: frac_compare("2/6", "1/3")?, percent: frac_read("12.5%")?.text, zero: frac_eval("1/(2-2)").error,}route GET /recipe = frac_scale_recipe(["2 1/4 cups flour", "3/4 cup sugar", "1 egg", { qty: "1/3", unit: "cup", item: "milk" }, "a pinch of salt"], "3/2")?$ quillo run fractions.qwe$ curl 'localhost:8080/workshop'$ curl 'localhost:8080/recipe'Source
//! Exact fractions for recipes, carpentry and school maths: "1 1/2", "3/8", "0.125" and "12.5%" read, added, multiplied and compared without rounding, whole expressions with brackets worked out, decimals turned into the nearest simple fraction (0.333 is 1/3), measures rounded to the nearest 1/16 inch, and recipes scaled.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};#[derive(Clone, Copy, Debug, PartialEq)]struct Q { n: i128, d: i128,}fn gcd(mut a: i128, mut b: i128) -> i128 { a = a.abs(); b = b.abs(); while b != 0 { let t = a % b; a = b; b = t; } a.max(1)}const LIMIT: i128 = 1_000_000_000_000_000_000; // 10^18: past this, an overflow errorimpl Q { fn new(n: i128, d: i128) -> Result<Q, String> { if d == 0 { return Err("division by zero".into()); } let g = gcd(n, d); let (mut n, mut d) = (n / g, d / g); if d < 0 { n = -n; d = -d; } if n.abs() > LIMIT || d > LIMIT { return Err("the numbers grew too large for an exact fraction".into()); } Ok(Q { n, d }) } fn add(self, o: Q) -> Result<Q, String> { Q::new(self.n.checked_mul(o.d).and_then(|a| o.n.checked_mul(self.d).and_then(|b| a.checked_add(b))).ok_or("overflow")?, self.d.checked_mul(o.d).ok_or("overflow")?) } fn neg(self) -> Q { Q { n: -self.n, d: self.d } } fn mul(self, o: Q) -> Result<Q, String> { let (g1, g2) = (gcd(self.n, o.d), gcd(o.n, self.d)); Q::new((self.n / g1).checked_mul(o.n / g2).ok_or("overflow")?, (self.d / g2).checked_mul(o.d / g1).ok_or("overflow")?) } fn inv(self) -> Result<Q, String> { Q::new(self.d, self.n) } fn pow(self, e: i128) -> Result<Q, String> { if e.abs() > 64 { return Err("powers up to 64".into()); } let mut r = Q { n: 1, d: 1 }; for _ in 0..e.abs() { r = r.mul(self)?; } if e < 0 { r.inv() } else { Ok(r) } } fn f(self) -> f64 { self.n as f64 / self.d as f64 } fn text(self) -> String { if self.d == 1 { self.n.to_string() } else { format!("{}/{}", self.n, self.d) } } fn mixed(self) -> String { if self.d == 1 || self.n.abs() < self.d { return self.text(); } let w = self.n / self.d; let r = (self.n % self.d).abs(); if r == 0 { w.to_string() } else { format!("{} {}/{}", w, r, self.d) } } fn json(self) -> Value { json!({ "num": self.n as i64, "den": self.d as i64, "text": self.text(), "mixed": self.mixed(), "decimal": self.f() }) }}/// A decimal written out ("0.125", "-2.5", "1e-3") as an exact fraction.fn decimal(s: &str) -> Result<Q, String> { let s = s.trim(); let (mant, exp) = match s.to_lowercase().split_once('e') { Some((m, e)) => (m.to_string(), e.parse::<i32>().map_err(|_| format!("{:?} is not a number", s))?), None => (s.to_string(), 0), }; let neg = mant.starts_with('-'); let m = mant.trim_start_matches(['-', '+']); let (int, frac) = m.split_once('.').unwrap_or((m, "")); if int.is_empty() && frac.is_empty() || !int.chars().chain(frac.chars()).all(|c| c.is_ascii_digit()) || int.len() + frac.len() > 18 { return Err(format!("{:?} is not a number", s)); } let n: i128 = format!("{}{}", int, frac).parse().unwrap_or(0); let mut q = Q::new(if neg { -n } else { n }, 10i128.pow(frac.len() as u32))?; if exp != 0 { q = q.mul(Q::new(10, 1)?.pow(exp as i128)?)?; } Ok(q)}/// "3/4", "1 1/2", "-2 3/8", "0.375", "12.5%", "⅜" as a fraction.fn parse(s: &str) -> Result<Q, String> { let s = s.trim(); let vulgar = [('½', (1, 2)), ('⅓', (1, 3)), ('⅔', (2, 3)), ('¼', (1, 4)), ('¾', (3, 4)), ('⅕', (1, 5)), ('⅖', (2, 5)), ('⅗', (3, 5)), ('⅘', (4, 5)), ('⅙', (1, 6)), ('⅚', (5, 6)), ('⅛', (1, 8)), ('⅜', (3, 8)), ('⅝', (5, 8)), ('⅞', (7, 8))]; if let Some((c, (n, d))) = vulgar.iter().find(|(c, _)| s.ends_with(*c)) { let whole = s.trim_end_matches(*c).trim(); let w = if whole.is_empty() { Q { n: 0, d: 1 } } else { parse(whole)? }; let f = Q::new(*n, *d)?; return if w.n < 0 { w.add(f.neg()) } else { w.add(f) }; } if let Some(p) = s.strip_suffix('%') { return decimal(p)?.mul(Q::new(1, 100)?); } if let Some((a, b)) = s.split_once('/') { let a = a.trim(); let den = decimal(b)?; // "1 1/2": a whole number, a space, a fraction. if let Some((w, n)) = a.rsplit_once(' ') { let w = decimal(w)?; let f = decimal(n)?.mul(den.inv()?)?; return if w.n < 0 { w.add(f.neg()) } else { w.add(f) }; } return decimal(a)?.mul(den.inv()?); } decimal(s)}fn arg(v: &Value) -> Result<Q, String> { match v { Value::String(s) => parse(s), Value::Number(n) => match n.as_i64() { Some(i) => Q::new(i as i128, 1), None => decimal(&n.to_string()), }, Value::Object(m) => Q::new(m.get("num").and_then(Value::as_i64).ok_or("a fraction map has num")? as i128, m.get("den").and_then(Value::as_i64).unwrap_or(1) as i128), other => Err(format!("{} is not a fraction", other)), }}/// A fraction read: `"3/4"`, `"1 1/2"`, `"0.125"`, `"12.5%"`, `"2⅜"`, a number, or `{ num, den }`. Gives `{ num, den, text: "3/2", mixed: "1 1/2", decimal }`, always in lowest terms.pub fn frac_read(value: serde_json::Value) -> Result<serde_json::Value, String> { Ok(arg(&value)?.json())}/// `a` plus, minus, times or divided by `b` (`op` "+", "-", "*", "/"), exactly.pub fn frac_calc(a: serde_json::Value, op: &str, b: serde_json::Value) -> Result<serde_json::Value, String> { let (x, y) = (arg(&a)?, arg(&b)?); Ok(match op { "+" => x.add(y)?, "-" => x.add(y.neg())?, "*" | "×" => x.mul(y)?, "/" | "÷" => x.mul(y.inv()?)?, other => return Err(format!("{:?}: +, -, * or /", other)), } .json())}/// -1, 0 or 1 as `a` is smaller than, equal to or larger than `b`, exactly (1/3 and 2/6 are equal).pub fn frac_compare(a: serde_json::Value, b: serde_json::Value) -> Result<i64, String> { let d = arg(&a)?.add(arg(&b)?.neg())?; Ok(d.n.signum() as i64)}struct P<'a> { t: Vec<&'a str>, i: usize,}fn tokens(s: &str) -> Vec<&str> { let mut out = vec![]; let b = s.as_bytes(); let mut i = 0; while i < b.len() { let c = b[i] as char; if c.is_whitespace() { i += 1; } else if "+-*/()^".contains(c) { out.push(&s[i..i + 1]); i += 1; } else { let st = i; while i < b.len() && !"+-*/()^ \t".contains(b[i] as char) { i += 1; } out.push(&s[st..i]); } } out}impl<'a> P<'a> { fn peek(&self) -> Option<&'a str> { self.t.get(self.i).copied() } fn expr(&mut self) -> Result<Q, String> { let mut v = self.term()?; while let Some(op) = self.peek().filter(|o| *o == "+" || *o == "-") { self.i += 1; let r = self.term()?; v = if op == "+" { v.add(r)? } else { v.add(r.neg())? }; } Ok(v) } fn term(&mut self) -> Result<Q, String> { let mut v = self.power()?; while let Some(op) = self.peek().filter(|o| *o == "*" || *o == "/") { self.i += 1; let r = self.power()?; v = if op == "*" { v.mul(r)? } else { v.mul(r.inv()?)? }; } Ok(v) } fn power(&mut self) -> Result<Q, String> { let b = self.unary()?; if self.peek() == Some("^") { self.i += 1; let e = self.unary()?; if e.d != 1 { return Err("powers are whole numbers".into()); } return b.pow(e.n); } Ok(b) } fn unary(&mut self) -> Result<Q, String> { match self.peek() { Some("-") => { self.i += 1; Ok(self.unary()?.neg()) } Some("(") => { self.i += 1; let v = self.expr()?; if self.peek() != Some(")") { return Err("a ( without its )".into()); } self.i += 1; Ok(v) } Some(t) => { self.i += 1; parse(t) } None => Err("the expression ends too soon".into()), } }}/// An expression worked out exactly: `"1/2 + 3/4 * (2/3 - 1/6)"`, with `+ - * / ^` and brackets; a `/` between plain numbers is division, so write mixed numbers as `(1 + 1/2)`.pub fn frac_eval(expression: &str) -> Result<serde_json::Value, String> { if expression.len() > 2000 { return Err("at most 2,000 characters".into()); } let mut p = P { t: tokens(expression), i: 0 }; let v = p.expr()?; if p.i != p.t.len() { return Err(format!("unexpected {:?}", p.t[p.i])); } Ok(v.json())}/// The simplest fraction within `max_den` of a decimal (continued fractions): 0.333 is 1/3, 3.14159 with 1000 is 355/113. Gives it with its `error`.pub fn frac_from_decimal(x: f64, max_den: i64) -> Result<serde_json::Value, String> { if !x.is_finite() { return Err("not a number".into()); } let max_den = max_den.clamp(1, 1_000_000_000) as i128; let (mut h0, mut h1, mut k0, mut k1) = (0i128, 1i128, 1i128, 0i128); let mut r = x; for _ in 0..64 { let a = r.floor(); if a.abs() > 1e15 { break; } let a = a as i128; let (h2, k2) = (a * h1 + h0, a * k1 + k0); if k2 > max_den { // The best semiconvergent within the limit. let t = (max_den - k0) / k1.max(1); let (hs, ks) = (t * h1 + h0, t * k1 + k0); if ks > 0 && (x - hs as f64 / ks as f64).abs() < (x - h1 as f64 / k1 as f64).abs() { h1 = hs; k1 = ks; } break; } h0 = h1; h1 = h2; k0 = k1; k1 = k2; let frac = r - a as f64; if frac.abs() < 1e-12 { break; } r = 1.0 / frac; } let q = Q::new(h1, k1)?; let mut v = q.json(); v["error"] = json!(x - q.f()); Ok(v)}/// A measure rounded to the nearest 1/`den` (16 for inches): 3.3 gives "3 5/16". With `unit` added to the text ("3 5/16 in").pub fn frac_round(x: f64, den: i64, unit: &str) -> Result<String, String> { if !x.is_finite() || !(1..=1024).contains(&den) { return Err("a number, and a denominator from 1 to 1024".into()); } let q = Q::new((x * den as f64).round() as i128, den as i128)?; let m = q.mixed(); Ok(if unit.is_empty() { m } else { format!("{} {}", m, unit) })}/// A recipe's quantities scaled by a factor ("3/2", 2, "0.5"): each item `{ qty: "1 1/2", unit, item }` (or text like "3/4 cup sugar"), quantities kept as kitchen fractions.pub fn frac_scale_recipe(items: Vec<serde_json::Value>, factor: serde_json::Value) -> Result<Vec<serde_json::Value>, String> { let f = arg(&factor)?; items .iter() .map(|it| { let (qty, rest) = match it { Value::String(s) => { // The quantity is the leading words that read as numbers: "1 1/2 cups flour". let words: Vec<&str> = s.split_whitespace().collect(); let mut k = 0; while k < words.len() && k < 2 && parse(&words[..=k].join(" ")).is_ok() { k += 1; } if k == 0 { return Ok(json!({ "text": s })); } (parse(&words[..k].join(" "))?, words[k..].join(" ")) } Value::Object(m) => (arg(m.get("qty").ok_or("an item needs its qty")?)?, format!("{} {}", m.get("unit").and_then(Value::as_str).unwrap_or(""), m.get("item").and_then(Value::as_str).unwrap_or("")).trim().to_string()), other => return Err(format!("an item is text or a map, not {}", other)), }; let q = qty.mul(f)?; // Kitchen-friendly: the nearest 1/8 when the exact fraction is awkward. let shown = if q.d <= 8 || [3, 6].contains(&q.d) { q.mixed() } else { Q::new((q.f() * 8.0).round() as i128, 8)?.mixed() }; Ok(json!({ "qty": shown, "exact": q.text(), "text": format!("{} {}", shown, rest).trim().to_string() })) }) .collect()}Related
bignum.qwer · 1.0.0Integers and fractions of any size, exact: factorials, powers, gcd, modular powers, and fractions such as 1/3 + 1/6 = 1/2 with no rounding.8 views · 7 downloadsFinance finance.qwer · 1.0.0Loan payments and amortization tables, compound growth with deposits, net present value and internal rate of return, in cents.7 views · 6 downloadsUnit conversion units.qwer · 1.0.0Quantities converted between units: length, mass, temperature, volume and kitchen measures, speed, energy, pressure, data sizes and more, with prefixes and compound units, mpg against L/100km, "5 ft 11 in" read, and values shown in their most readable unit.7 views · 8 downloadsClustering clustering.qwer · 1.0.0Data put into groups: k-means with k chosen by the elbow and silhouette, DBSCAN for groups of any shape and their outliers (in metres for latitude and longitude), and hierarchical clustering, on rows of numbers or maps.7 views · 7 downloadsFrequency spectrum fft.qwer · 1.0.0The frequency spectrum of samples from a microphone or sensor: frequencies and their strength, the dominant ones found between bins, and the signal's level.8 views · 7 downloadsGraphs graph.qwer · 1.0.0Graphs given as edges: shortest paths with distances, task order by topological sort with cycles named, connected groups, and what a node reaches.7 views · 7 downloadsThread 0 posts
On Fractions: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.