Misc · Numbers: a result goes on whole; long readings wrap

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 01:06:32 -04:00
1 parent 73f3399cea
commit 5e7fc24db9
5 files changed
+126 -68

No files matched your search

+9 -1
View File
@@ -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")),
))
}
+75 -48
View File
@@ -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<String>,
problem: Option<String>,
}
@@ -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<Tape>, 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<Key> {
}
}
/// 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");
}
}
+17 -3
View File
@@ -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"
);
}
+23 -14
View File
@@ -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<TextInput>,
@@ -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 {
+2 -2
View File
@@ -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