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:
1 parent
73f3399cea
commit
5e7fc24db9
5 files changed
+126
-68
No files matched your search
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user