From 5e7fc24db98cde629741ef27e1e04f4dfaf60480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 28 Sep 2026 01:06:32 -0400 Subject: [PATCH] =?UTF-8?q?Misc=20=C2=B7=20Numbers:=20a=20result=20goes=20?= =?UTF-8?q?on=20whole;=20long=20readings=20wrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A calculator result stays in the sum as `a`, handed to evaluate with its whole value, so 1 ÷ 3 = × 3 = gives 1; sign turns it and delete takes it. Equals waits on a dangling operator. The converter's reading wraps in its box rather than ending in an ellipsis, and tests hold Control, the = key, a point after a result and the six digits. --- examples/gallery/pages/misc/mod.rs | 10 ++- src/misc/calculator.rs | 123 ++++++++++++++++++----------- src/misc/tests/numbers.rs | 20 ++++- src/misc/units.rs | 37 +++++---- tasks/ch31-43.md | 4 +- 5 files changed, 126 insertions(+), 68 deletions(-) diff --git a/examples/gallery/pages/misc/mod.rs b/examples/gallery/pages/misc/mod.rs index cae9e5c..8404dd9 100644 --- a/examples/gallery/pages/misc/mod.rs +++ b/examples/gallery/pages/misc/mod.rs @@ -27,6 +27,11 @@ pub const PAGE: Page = Page { Step::Shot("calculator-typed"), Step::Key("enter"), Step::Shot("calculator-result"), + Step::DownAt("converter", 24.0, 60.0), + Step::UpAt("converter", 24.0, 60.0), + Step::Key("cmd-a"), + Step::Type("1000000000000000"), + Step::Shot("converter-long"), ], }; @@ -96,5 +101,8 @@ fn converter(cx: &App) -> impl IntoElement + use<> { "An amount read in another unit: length, area, volume, mass, temperature, speed and data, with a swap.", cx, ) - .child(div().w(px(280.0)).child(UnitConverter::new("unit-converter"))) + .child(probe( + "converter", + div().w(px(280.0)).child(UnitConverter::new("unit-converter")), + )) } diff --git a/src/misc/calculator.rs b/src/misc/calculator.rs index 380177d..d1ee998 100644 --- a/src/misc/calculator.rs +++ b/src/misc/calculator.rs @@ -51,10 +51,14 @@ const PAD: [(Key, &str); 19] = [ const OPS: [char; 4] = ['+', '-', '*', '/']; -/// The entry as typed, in ASCII; the sum that gave it while it is a result; and why that sum failed. +/// Stands for the last result in the entry, so a sum goes on with its whole value. +const ANSWER: char = 'a'; + +/// The entry as typed, in ASCII, where `a` is the last result, kept whole in `answer`; the sum that gave it, as shown; and why a sum failed. #[derive(Default)] struct Tape { entry: String, + answer: f64, worked: Option, problem: Option, } @@ -75,9 +79,12 @@ impl Tape { } fn press(&mut self, key: Key) { - let failed = self.problem.take().is_some(); - let result = self.worked.take().is_some() && !failed; - if failed || (result && matches!(key, Key::Digit(_) | Key::Point)) { + if self.problem.take().is_some() { + self.entry.clear(); + } + self.worked = None; + let answered = self.entry.ends_with(ANSWER); + if answered && matches!(key, Key::Digit(_) | Key::Point) { self.entry.clear(); } let start = self.number_start(); @@ -107,6 +114,7 @@ impl Tape { } } } + Key::Sign if answered => self.answer = -self.answer, Key::Sign if number.starts_with('-') => { self.entry.remove(start); } @@ -116,29 +124,20 @@ impl Tape { self.entry.pop(); } Key::Clear => self.entry.clear(), - Key::Equals if self.entry.is_empty() => {} - Key::Equals => match evaluate(&self.entry, &[]) { - Ok(value) => self.worked = Some(std::mem::replace(&mut self.entry, plain(value))), - Err(problem) => { - self.worked = Some(self.entry.clone()); - self.problem = Some(problem); + Key::Equals if self.entry.is_empty() || self.entry.ends_with(OPS) => {} + Key::Equals => { + self.worked = Some(format!("{} =", shown(&self.entry, self.answer))); + match evaluate(&self.entry, &[(ANSWER.to_string().into(), self.answer)]) { + Ok(value) => (self.entry, self.answer) = (ANSWER.to_string(), value), + Err(problem) => self.problem = Some(problem), } - }, + } } } } -/// A result as the entry types it: twelve significant digits, no groups, an ASCII minus. -fn plain(value: f64) -> String { - significant(value, 12, Separators::EN) - .chars() - .filter(|ch| *ch != ',') - .map(|ch| if ch == MINUS { '-' } else { ch }) - .collect() -} - -/// The entry as it reads: digits grouped, and ×, ÷ and a true minus between spaced terms. -fn shown(entry: &str) -> String { +/// The entry as it reads: digits grouped, the last result to twelve significant digits, and ×, ÷ and a true minus between spaced terms. +fn shown(entry: &str, answer: f64) -> String { let (mut out, mut number, mut after_op) = (String::new(), String::new(), true); let flush = |out: &mut String, number: &mut String| { let (whole, part) = number.split_once('.').unwrap_or((number, "")); @@ -150,6 +149,11 @@ fn shown(entry: &str) -> String { number.clear(); }; for ch in entry.chars() { + if ch == ANSWER { + out.push_str(&significant(answer, 12, Separators::EN)); + after_op = false; + continue; + } if ch.is_ascii_digit() || ch == '.' { number.push(ch); after_op = false; @@ -166,13 +170,17 @@ fn shown(entry: &str) -> String { after_op = true; } flush(&mut out, &mut number); - out + out.trim_end().to_string() } fn hit(tape: &Entity, key: Key, cx: &mut App) { tape.update(cx, |tape, cx| { tape.press(key); - log::info!("calculator: {key:?}, entry {:?}", tape.entry); + log::info!( + "calculator: {key:?}, entry {:?}, answer {}", + tape.entry, + tape.answer + ); cx.notify(); }); } @@ -197,7 +205,7 @@ fn typed(event: &KeyDownEvent, own: bool) -> Option { } } -/// A calculator: the sum as you type it over its result, and a pad of digits, the four operations, sign, delete and clear. Keys type too: digits, + − * /, = or Enter, Backspace and Escape. It works in precedence, × and ÷ before + and −, and shows twelve significant digits. +/// A calculator: the sum as you type it over its result, and a pad of digits, the four operations, sign, delete and clear. Keys type too: digits, `.`, `+ - * /`, `=` or Enter, Backspace and Escape; Equals waits while the sum ends in an operator. It works in precedence, × and ÷ before + and −, and shows twelve significant digits. #[derive(IntoElement)] pub struct Calculator { id: ElementId, @@ -221,13 +229,9 @@ impl RenderOnce for Calculator { let main = match (&tape.problem, tape.entry.is_empty()) { (Some(problem), _) => problem.clone(), (None, true) => "0".to_string(), - (None, false) => shown(&tape.entry), + (None, false) => shown(&tape.entry, tape.answer), }; - let above = tape - .worked - .as_deref() - .map(|sum| format!("{} =", shown(sum))); - (above, main, tape.problem.is_some()) + (tape.worked.clone(), main, tape.problem.is_some()) }; let keys = PAD.iter().map(|&(key, label)| { let button = match key { @@ -301,7 +305,7 @@ impl RenderOnce for Calculator { #[cfg(test)] mod tests { - use super::{Key, Tape, plain, shown}; + use super::{Key, Tape, shown}; fn typed(keys: &[Key]) -> Tape { let mut tape = Tape::default(); @@ -323,31 +327,50 @@ mod tests { .collect() } + /// What the calculator's main line reads after `keys`. + fn reads(keys: &str) -> String { + let tape = typed(&sum(keys)); + match tape.problem { + Some(problem) => problem, + None if tape.entry.is_empty() => "0".into(), + None => shown(&tape.entry, tape.answer), + } + } + #[test] - fn it_works_in_precedence_and_carries_the_result_on() { - let tape = typed(&sum("12+3*4=")); + fn it_works_in_precedence_and_goes_on_with_the_whole_result() { assert_eq!( - (tape.entry.as_str(), tape.worked.as_deref()), - ("24", Some("12+3*4")) + typed(&sum("12+3*4=")).worked.as_deref(), + Some("12 + 3 × 4 =") ); + assert_eq!(reads("12+3*4="), "24"); assert_eq!( - typed(&sum("12+3*4=-4=")).entry, + reads("12+3*4=-4="), "20", "an operator goes on from the result" ); - assert_eq!(typed(&sum("12+3*4=7")).entry, "7", "a digit starts afresh"); assert_eq!( - typed(&sum(".1+.2=")).entry, - "0.3", - "twelve digits hide binary noise" + reads("1/3=*3="), + "1", + "the whole result, not its twelve digits" ); - assert_eq!(typed(&sum("2/3=")).entry, "0.666666666667"); + assert_eq!(reads("12+3*4=7"), "7", "a digit starts afresh"); + assert_eq!(reads("12+3*4=.5"), "0.5", "so does a point"); + assert_eq!(reads("12+3*4=~+4="), "−20", "sign turns the result"); + assert_eq!(reads("12+3*4=<"), "0", "delete takes the whole result"); + assert_eq!(reads(".1+.2="), "0.3", "twelve digits hide binary noise"); + assert_eq!(reads("2/3="), "0.666666666667"); } #[test] fn keys_keep_the_entry_a_sum() { assert_eq!(typed(&sum("007")).entry, "7", "no leading zeros"); assert_eq!(typed(&sum("1..5.")).entry, "1.5", "one point a number"); + assert_eq!( + typed(&sum(".5+.5")).entry, + "0.5+0.5", + "a point starts with a zero" + ); assert_eq!( typed(&sum("5+*")).entry, "5*", @@ -366,21 +389,25 @@ mod tests { "a dangling sign goes with the operator" ); assert_eq!(typed(&sum("12+3C")).entry, ""); + assert_eq!( + reads("1234+5*="), + "1,234 + 5 ×", + "Equals waits on a dangling operator" + ); } #[test] fn a_failed_sum_says_why_and_the_next_key_starts_afresh() { - let tape = typed(&sum("5/0=")); - assert_eq!(tape.problem.as_deref(), Some("division by zero")); + assert_eq!(reads("5/0="), "division by zero"); + assert_eq!(typed(&sum("5/0=")).worked.as_deref(), Some("5 ÷ 0 =")); assert_eq!(typed(&sum("5/0=8")).entry, "8"); assert_eq!(typed(&sum("5/0=+")).entry, "", "no result to go on from"); } #[test] fn the_entry_reads_grouped_with_true_signs() { - assert_eq!(shown("-1234.5*-6/7-8"), "−1,234.5 × −6 ÷ 7 − 8"); - assert_eq!(shown("0."), "0."); - assert_eq!(plain(-1_234_567.0), "-1234567"); - assert_eq!(plain(1e-15), "0.000000000000001"); + assert_eq!(shown("-1234.5*-6/7-8", 0.0), "−1,234.5 × −6 ÷ 7 − 8"); + assert_eq!(shown("0.", 0.0), "0."); + assert_eq!(shown("a*2", -1_234.5), "−1,234.5 × 2"); } } diff --git a/src/misc/tests/numbers.rs b/src/misc/tests/numbers.rs index 6fd52e1..1317390 100644 --- a/src/misc/tests/numbers.rs +++ b/src/misc/tests/numbers.rs @@ -27,12 +27,22 @@ fn calculator() -> AnyElement { fn a_calculator_takes_typed_keys(cx: &mut TestAppContext) { let (_, cx) = stage(calculator, cx); press("tab", cx); - for key in ["1", "2", "+", "3", "*", "4", "cmd-backspace", "5"] { + for key in [ + "1", + "2", + "+", + "3", + "*", + "4", + "cmd-backspace", + "ctrl-backspace", + "5", + ] { press(key, cx); } assert!( shown("calculator-12 + 3 × 45", cx), - "Command left the delete to the app" + "Command and Control left the delete to the app" ); press("backspace", cx); for _ in 0..5 { @@ -51,12 +61,16 @@ fn a_calculator_takes_typed_keys(cx: &mut TestAppContext) { shown("calculator-153", cx), "Enter on the calculator is equals" ); + for key in ["/", "9", "="] { + press(key, cx); + } + assert!(shown("calculator-17", cx), "= is equals too"); press("tab", cx); press("enter", cx); press("+", cx); press("5", cx); assert!( - shown("calculator-5", cx) && !shown("calculator-153 + 5", cx), + shown("calculator-5", cx) && !shown("calculator-17 + 5", cx), "Enter on the focused AC cleared the result" ); } diff --git a/src/misc/units.rs b/src/misc/units.rs index 2851249..3764e38 100644 --- a/src/misc/units.rs +++ b/src/misc/units.rs @@ -9,7 +9,6 @@ use crate::{ primitives::IconName, theme::{ActiveTheme, ControlSize, Radius}, typography::{ - Ellipsis, format::{MINUS, Separators, significant}, tabular, }, @@ -144,6 +143,12 @@ pub(crate) fn convert(amount: f64, from: &Unit, to: &Unit) -> f64 { (amount * from.scale + from.offset - to.offset) / to.scale } +/// `amount` in `from` as it reads in `to`: six significant digits and the symbol. +fn read(amount: f64, from: &Unit, to: &Unit) -> String { + let value = significant(convert(amount, from, to), 6, Separators::EN); + format!("{value} {}", to.symbol) +} + /// The converter's own field, kind and units. struct Desk { input: Entity, @@ -160,7 +165,7 @@ fn place(units: &[Unit], name: &str) -> usize { .unwrap_or_else(|| panic!("unit converter: no unit named {name}")) } -/// An amount in one unit read in another: length, area, volume, mass, temperature, speed or data, with a swap of the two units. It shows six significant digits. +/// An amount in one unit read in another: length, area, volume, mass, temperature, speed or data, with a swap of the two units. It shows six significant digits, wrapped in its box when long. #[derive(IntoElement)] pub struct UnitConverter { id: ElementId, @@ -193,15 +198,7 @@ impl RenderOnce for UnitConverter { let text = input.read(cx).text().to_string(); let amount = parse_number(&text); let result = match amount { - Some(amount) => format!( - "{} {}", - significant( - convert(amount, &kind.units[from], &kind.units[to]), - 6, - Separators::EN - ), - kind.units[to].symbol - ), + Some(amount) => read(amount, &kind.units[from], &kind.units[to]), None => "—".to_string(), }; let theme = cx.theme(); @@ -288,8 +285,9 @@ impl RenderOnce for UnitConverter { div() .flex() .items_center() - .h(theme.control_height(ControlSize::Md)) + .min_h(theme.control_height(ControlSize::Md)) .px(theme.control_padding(ControlSize::Md)) + .py_1() .rounded(theme.radius(Radius::Md)) .bg(theme.colors.sunken) .text_color(theme.colors.fg) @@ -298,7 +296,7 @@ impl RenderOnce for UnitConverter { .flex_1() .min_w_0() .debug_selector(|| format!("converted-{result}")) - .child(Ellipsis::new(result.clone())), + .child(result.clone()), ), ), ) @@ -315,7 +313,7 @@ impl RenderOnce for UnitConverter { #[cfg(test)] mod tests { - use super::{KINDS, Kind, convert, place}; + use super::{KINDS, Kind, convert, place, read}; fn between(kind: &Kind, amount: f64, from: &str, to: &str) -> f64 { convert( @@ -359,6 +357,17 @@ mod tests { )); } + #[test] + fn a_reading_keeps_six_significant_digits_and_the_symbol() { + let length = &KINDS[0]; + let unit = |name: &str| &length.units[place(length.units, name)]; + assert_eq!(read(1.0, unit("Mile"), unit("Kilometer")), "1.60934 km"); + assert_eq!( + read(1e9, unit("Meter"), unit("Millimeter")), + "1,000,000,000,000 mm" + ); + } + #[test] fn each_kind_starts_on_two_units_of_its_own() { for kind in &KINDS { diff --git a/tasks/ch31-43.md b/tasks/ch31-43.md index 0efb01d..48f97cb 100644 --- a/tasks/ch31-43.md +++ b/tasks/ch31-43.md @@ -315,8 +315,8 @@ ## T42b Misc · Numbers -- [x] Calculator → `misc::Calculator`: digits, the four operations in precedence through `forms::evaluate`, sign, delete and clear, twelve significant digits; keys type too, and Enter gives the result while the calculator itself has focus -- [x] UnitConverter → `misc::UnitConverter`: length, area, volume, mass, temperature, speed and data, each on a starting pair, with a swap; six significant digits +- [x] Calculator → `misc::Calculator`: digits, the four operations in precedence through `forms::evaluate`, sign, delete and clear; a result goes on at its whole value and shows twelve significant digits; keys type too, and Enter gives the result while the calculator itself has focus +- [x] UnitConverter → `misc::UnitConverter`: length, area, volume, mass, temperature, speed and data, each on a starting pair, with a swap; six significant digits, wrapped when long ## T42c Misc · Codes and checks