| | | @@ 変更前 1行目〜 / 変更後 1行目〜 @@ |
| 1 | 1 | | //! 洗濯洗剤・柔軟剤の量。 |
| 2 | | − | //! v1: 元ツール(app.js)と同じ考え方を、Rust でいちばん普通に書いた形。f64 に読み、水量 ÷ 基準の水量 × 基準の量 を計算し、 |
| 3 | | − | //! (x * 10.0).round() / 10.0 で小数第1位に丸める。 |
| 2 | + | //! v2: f64 を使わない。入力の文字列を「整数 ÷ 10^小数の桁数」として10進のまま読み、水量・必要量を分数 p / q(整数)で持つ。 |
| 3 | + | //! 小数第1位への四捨五入は p × 10 ÷ q の余りが半分以上なら繰り上げる(0.5 ちょうどは大きい方へ)。 |
| 4 | + | //! v1 は f64 で 13.2 ÷ 40 × 25 が 8.2499999999999982 になり、(x * 10.0).round() で 8.3 が 8.2 になっていた(11.6L の 7.25 も同じ)。 |
| 5 | + | //! parse::<f64>() は "1e2"・"inf"・"NaN" も読むので、入力は 10進の数の形([0-9]+(\.[0-9]+)?)かを自分で確かめる。 |
| 4 | 6 | | |
| 5 | 7 | | use crate::json::{obj, s, Value}; |
| 6 | 8 | | |
| 9 | + | const OVERFLOW: &str = "桁あふれ"; |
| 10 | + | |
| 7 | 11 | | /// 1kg あたりの水(L) |
| 8 | | − | fn water_per_kg(machine: &str) -> Option<f64> { |
| 12 | + | fn water_per_kg(machine: &str) -> Option<u128> { |
| 9 | 13 | | match machine { |
| 10 | | − | "top" => Some(10.0), |
| 11 | | − | "drum" => Some(6.0), |
| 14 | + | "top" => Some(10), |
| 15 | + | "drum" => Some(6), |
| 12 | 16 | | _ => None, |
| 13 | 17 | | } |
| 14 | 18 | | } |
| 15 | 19 | | |
| 16 | | − | /// 小数第1位まで(小数第1位が 0 なら整数で。f64 の Display は 15.0 を "15" と書く) |
| 17 | | − | fn fmt(x: f64) -> String { |
| 18 | | − | format!("{}", (x * 10.0).round() / 10.0) |
| 20 | + | /// 10進の数の文字列を (整数, 10^小数の桁数) に。形が違えば None。桁が u128 に収まらなければ Err |
| 21 | + | fn dec(text: &str) -> Result<Option<(u128, u128)>, String> { |
| 22 | + | let (int_part, frac_part) = match text.split_once('.') { Some((i, f)) => (i, Some(f)), None => (text, None) }; |
| 23 | + | let digits = |p: &str| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()); |
| 24 | + | if !digits(int_part) || frac_part.is_some_and(|f| !digits(f)) { |
| 25 | + | return Ok(None); |
| 26 | + | } |
| 27 | + | let frac = frac_part.unwrap_or(""); |
| 28 | + | let mut v: u128 = 0; |
| 29 | + | for b in int_part.bytes().chain(frac.bytes()) { |
| 30 | + | v = v.checked_mul(10).and_then(|x| x.checked_add((b - b'0') as u128)).ok_or(OVERFLOW)?; |
| 31 | + | } |
| 32 | + | let scale = 10u128.checked_pow(frac.len() as u32).ok_or(OVERFLOW)?; |
| 33 | + | Ok(Some((v, scale))) |
| 19 | 34 | | } |
| 20 | 35 | | |
| 21 | | − | /// 必要量 = 水量 ÷ 基準の水量 × 基準の量(基準が読めない・基準の水量が 0 以下なら null) |
| 22 | | − | fn amount(water: f64, ref_water: Option<&str>, ref_amount: Option<&str>) -> Value { |
| 23 | | − | let (Some(Ok(rw)), Some(Ok(ra))) = (ref_water.map(str::parse::<f64>), ref_amount.map(str::parse::<f64>)) else { |
| 24 | | − | return Value::Null; |
| 25 | | − | }; |
| 26 | | − | if rw <= 0.0 { |
| 27 | | − | return Value::Null; |
| 36 | + | fn mul(a: u128, b: u128) -> Result<u128, String> { |
| 37 | + | a.checked_mul(b).ok_or(OVERFLOW.into()) |
| 38 | + | } |
| 39 | + | |
| 40 | + | /// p ÷ q(p ≥ 0・q > 0)を小数第1位に四捨五入して書く(小数第1位が 0 なら整数で) |
| 41 | + | fn fmt1(p: u128, q: u128) -> Result<String, String> { |
| 42 | + | let a = mul(p, 10)?; |
| 43 | + | let (t, r) = (a / q, a % q); |
| 44 | + | let tenths = if r >= q - r { t + 1 } else { t }; // 余り × 2 ≥ q(× 2 であふれないよう引き算で比べる) |
| 45 | + | Ok(if tenths % 10 == 0 { format!("{}", tenths / 10) } else { format!("{}.{}", tenths / 10, tenths % 10) }) |
| 46 | + | } |
| 47 | + | |
| 48 | + | /// 必要量 = 水量 × 基準の量 ÷ 基準の水量(基準が読めない・基準の水量が 0 以下なら null) |
| 49 | + | fn amount((wp, wq): (u128, u128), ref_water: Option<&str>, ref_amount: Option<&str>) -> Result<Value, String> { |
| 50 | + | let (Some(rw), Some(ra)) = (ref_water, ref_amount) else { return Ok(Value::Null) }; |
| 51 | + | let (Some((rwv, rws)), Some((rav, ras))) = (dec(rw)?, dec(ra)?) else { return Ok(Value::Null) }; |
| 52 | + | if rwv == 0 { |
| 53 | + | return Ok(Value::Null); |
| 28 | 54 | | } |
| 29 | | − | s(fmt(water / rw * ra)) |
| 55 | + | // (wp/wq) × (rav/ras) ÷ (rwv/rws) = wp·rav·rws ÷ (wq·ras·rwv) |
| 56 | + | Ok(s(fmt1(mul(mul(wp, rav)?, rws)?, mul(mul(wq, ras)?, rwv)?)?)) |
| 30 | 57 | | } |
| 31 | 58 | | |
| 32 | 59 | | pub fn calc( |
| | | @@ 変更前 37行目〜 / 変更後 64行目〜 @@ |
| 37 | 64 | | ref_amount: Option<&str>, |
| 38 | 65 | | soft_water: Option<&str>, |
| 39 | 66 | | soft_amount: Option<&str>, |
| 40 | | − | ) -> Value { |
| 41 | | − | let invalid = || obj(vec![("error", s("invalid_input"))]); |
| 42 | | − | let Ok(v) = value.parse::<f64>() else { return invalid() }; |
| 43 | | − | if v <= 0.0 { |
| 67 | + | ) -> Result<Value, String> { |
| 68 | + | let invalid = || Ok(obj(vec![("error", s("invalid_input"))])); |
| 69 | + | let Some((v, scale)) = dec(value)? else { return invalid() }; |
| 70 | + | if v == 0 { |
| 44 | 71 | | return invalid(); |
| 45 | 72 | | } |
| 46 | 73 | | let Some(per_kg) = water_per_kg(machine.unwrap_or("top")) else { return invalid() }; |
| 47 | 74 | | let water = match basis { |
| 48 | | − | "water" => v, |
| 49 | | − | "weight" => v * per_kg, |
| 75 | + | "water" => (v, scale), |
| 76 | + | "weight" => (mul(v, per_kg)?, scale), |
| 50 | 77 | | _ => return invalid(), |
| 51 | 78 | | }; |
| 52 | | − | let softener = if soft_water.is_some() { amount(water, soft_water, soft_amount) } else { Value::Null }; |
| 53 | | − | obj(vec![("water_l", s(fmt(water))), ("detergent", amount(water, ref_water, ref_amount)), ("softener", softener)]) |
| 79 | + | let softener = if soft_water.is_some() { amount(water, soft_water, soft_amount)? } else { Value::Null }; |
| 80 | + | Ok(obj(vec![ |
| 81 | + | ("water_l", s(fmt1(water.0, water.1)?)), |
| 82 | + | ("detergent", amount(water, ref_water, ref_amount)?), |
| 83 | + | ("softener", softener), |
| 84 | + | ])) |
| 54 | 85 | | } |