| | | @@ 変更前 3行目〜 / 変更後 3行目〜 @@ |
| 3 | 3 | | |
| 4 | 4 | | /** |
| 5 | 5 | | * 米の量と炊き上がり・人数。 |
| 6 | | − | * v1: 元ツールと同じく値を float に読み、掛け算・割り算を float のまま行って、round() で丸める素直な書き方。 |
| 7 | | − | * 5g 単位は round(x / 5) * 5、0.5合単位は round(x * 2) / 2、合の表示は round(x * 10) / 10 を整数なら整数で書く。 |
| 6 | + | * v2: 値を float に読まず、10進の数字の列(値 = 数字の列 ÷ 10^小数の桁数)のまま扱い、 |
| 7 | + | * 各項目を「数字の列 × 小さな整数 ÷ (小さな整数 × 10^桁数)」の正確な比として、0.5 ちょうどは大きい方へ四捨五入する。 |
| 8 | + | * float だと 375 × 2.3 が 862.4999… になって 5g 単位で 860g に落ち、Python などの偶数丸めは 157.5 ÷ 5 = 31.5 を 31 にする。 |
| 9 | + | * 桁数に上限を置かないため、途中の掛け算・足し算・割り算は数字の列の筆算で行う(bcmath・gmp は使わない)。 |
| 8 | 10 | | */ |
| 9 | | − | const G_PER_GOU = 150; |
| 10 | | − | const ML_PER_GOU = 180; |
| 11 | | − | const COOK_LO = 2.1; |
| 12 | | − | const COOK_MID = 2.2; |
| 13 | | − | const COOK_HI = 2.3; |
| 14 | | − | const BOWL_G = 150; |
| 15 | | − | const ONIGIRI_G = 105; |
| 11 | + | const DECIMAL = '/^[0-9]+(\.[0-9]+)?$/'; |
| 16 | 12 | | |
| 17 | | − | function round5(float $x): int |
| 13 | + | /** 数字の列 × 小さな整数(筆算) */ |
| 14 | + | function mulSmall(string $a, int $k): string |
| 18 | 15 | | { |
| 19 | | − | return (int) round($x / 5) * 5; |
| 16 | + | $out = ''; |
| 17 | + | $carry = 0; |
| 18 | + | for ($i = strlen($a) - 1; $i >= 0; $i--) { |
| 19 | + | $p = (int) $a[$i] * $k + $carry; |
| 20 | + | $out = ($p % 10) . $out; |
| 21 | + | $carry = intdiv($p, 10); |
| 22 | + | } |
| 23 | + | return ltrim(($carry ? (string) $carry : '') . $out, '0') ?: '0'; |
| 20 | 24 | | } |
| 21 | 25 | | |
| 22 | | − | function gouText(float $g): string |
| 26 | + | /** 数字の列どうしの足し算(筆算) */ |
| 27 | + | function addDigits(string $a, string $b): string |
| 23 | 28 | | { |
| 24 | | − | $r = round($g * 10) / 10; |
| 25 | | − | return $r == floor($r) ? (string) (int) $r : (string) $r; |
| 29 | + | $n = max(strlen($a), strlen($b)); |
| 30 | + | $a = str_pad($a, $n, '0', STR_PAD_LEFT); |
| 31 | + | $b = str_pad($b, $n, '0', STR_PAD_LEFT); |
| 32 | + | $out = ''; |
| 33 | + | $carry = 0; |
| 34 | + | for ($i = $n - 1; $i >= 0; $i--) { |
| 35 | + | $s = (int) $a[$i] + (int) $b[$i] + $carry; |
| 36 | + | $out = ($s % 10) . $out; |
| 37 | + | $carry = intdiv($s, 10); |
| 38 | + | } |
| 39 | + | return ltrim(($carry ? '1' : '') . $out, '0') ?: '0'; |
| 40 | + | } |
| 41 | + | |
| 42 | + | /** 数字の列 ÷ 小さな整数 の切り捨て(筆算) */ |
| 43 | + | function divSmall(string $a, int $k): string |
| 44 | + | { |
| 45 | + | $out = ''; |
| 46 | + | $r = 0; |
| 47 | + | for ($i = 0, $n = strlen($a); $i < $n; $i++) { |
| 48 | + | $r = $r * 10 + (int) $a[$i]; |
| 49 | + | $out .= intdiv($r, $k); |
| 50 | + | $r %= $k; |
| 51 | + | } |
| 52 | + | return ltrim($out, '0') ?: '0'; |
| 53 | + | } |
| 54 | + | |
| 55 | + | /** |
| 56 | + | * (digits ÷ 10^scale) × mul ÷ div を整数に四捨五入(0 以上なので 0.5 ちょうどは大きい方へ)。 |
| 57 | + | * = floor((2 × digits × mul + div × 10^scale) ÷ (2 × div × 10^scale))。10^scale で割るのは下の桁を落とすだけ |
| 58 | + | */ |
| 59 | + | function roundRatio(string $digits, int $scale, int $mul, int $div): int |
| 60 | + | { |
| 61 | + | $a = addDigits(mulSmall($digits, 2 * $mul), $div . str_repeat('0', $scale)); |
| 62 | + | $a = strlen($a) > $scale ? substr($a, 0, strlen($a) - $scale) : '0'; |
| 63 | + | return (int) divSmall($a, 2 * $div); |
| 64 | + | } |
| 65 | + | |
| 66 | + | /** 0.1合単位の整数を "3"・"2.5"・"0.2" の形にする */ |
| 67 | + | function gouText(int $tenths): string |
| 68 | + | { |
| 69 | + | return $tenths % 10 === 0 ? (string) intdiv($tenths, 10) : intdiv($tenths, 10) . '.' . $tenths % 10; |
| 26 | 70 | | } |
| 27 | 71 | | |
| 28 | 72 | | /** @param array{mode: string, value: string} $in */ |
| 29 | 73 | | function riceAmount(array $in): array |
| 30 | 74 | | { |
| 31 | | − | if (!is_numeric($in['value']) || !in_array($in['mode'], ['gou', 'servings', 'grams'], true)) { |
| 75 | + | $mode = $in['mode']; |
| 76 | + | $value = $in['value']; |
| 77 | + | if (!is_string($value) || !preg_match(DECIMAL, $value) || trim($value, '0.') === '' |
| 78 | + | || !in_array($mode, ['gou', 'servings', 'grams'], true)) { |
| 32 | 79 | | return ['error' => 'invalid_input']; |
| 33 | 80 | | } |
| 34 | | − | $v = (float) $in['value']; |
| 35 | | − | if ($v <= 0) { |
| 36 | | − | return ['error' => 'invalid_input']; |
| 37 | | − | } |
| 38 | | − | if ($in['mode'] === 'servings') { |
| 39 | | − | $gou = round($v * BOWL_G / (G_PER_GOU * COOK_MID) * 2) / 2; // 0.5合単位 |
| 40 | | − | if ($gou < 0.5) { |
| 41 | | − | $gou = 0.5; |
| 42 | | − | } |
| 43 | | − | $g = $gou * G_PER_GOU; |
| 44 | | − | } elseif ($in['mode'] === 'grams') { |
| 45 | | − | $g = $v; |
| 46 | | − | $gou = $g / G_PER_GOU; |
| 81 | + | [$int, $frac] = explode('.', $value . '.'); |
| 82 | + | $n = ltrim($int . $frac, '0'); // 値 = $n ÷ 10^$s |
| 83 | + | $s = strlen($frac); |
| 84 | + | if ($mode === 'servings') { |
| 85 | + | // 合 = 杯数 × 150 ÷ (150 × 2.2) = 杯数 × 10 ÷ 22。0.5合単位 = (合 × 2) を四捨五入した半合の数 |
| 86 | + | $halves = max(roundRatio($n, $s, 10, 11), 1); |
| 87 | + | $tenths = $halves * 5; |
| 88 | + | [$g, $gs] = [(string) ($halves * 75), 0]; // 生米 = 半合 × 75g |
| 89 | + | } elseif ($mode === 'grams') { |
| 90 | + | $tenths = roundRatio($n, $s, 1, 15); // g ÷ 150 × 10 |
| 91 | + | [$g, $gs] = [$n, $s]; |
| 47 | 92 | | } else { |
| 48 | | − | $gou = $v; |
| 49 | | − | $g = $gou * G_PER_GOU; |
| 93 | + | $tenths = roundRatio($n, $s, 10, 1); |
| 94 | + | [$g, $gs] = [mulSmall($n, 150), $s]; |
| 50 | 95 | | } |
| 51 | 96 | | return [ |
| 52 | | − | 'gou' => gouText($gou), |
| 53 | | − | 'rice_g' => (int) round($g), |
| 54 | | − | 'ml' => (int) round($g / G_PER_GOU * ML_PER_GOU), |
| 55 | | − | 'cooked_low' => round5($g * COOK_LO), |
| 56 | | − | 'cooked_high' => round5($g * COOK_HI), |
| 57 | | − | 'bowls' => (int) round($g * COOK_MID / BOWL_G), |
| 58 | | − | 'onigiri' => (int) round($g * COOK_MID / ONIGIRI_G), |
| 97 | + | 'gou' => gouText($tenths), |
| 98 | + | 'rice_g' => roundRatio($g, $gs, 1, 1), |
| 99 | + | 'ml' => roundRatio($g, $gs, 6, 5), // g ÷ 150 × 180 |
| 100 | + | 'cooked_low' => roundRatio($g, $gs, 21, 50) * 5, // g × 2.1 ÷ 5 |
| 101 | + | 'cooked_high' => roundRatio($g, $gs, 23, 50) * 5, // g × 2.3 ÷ 5 |
| 102 | + | 'bowls' => roundRatio($g, $gs, 11, 750), // g × 2.2 ÷ 150 |
| 103 | + | 'onigiri' => roundRatio($g, $gs, 11, 525), // g × 2.2 ÷ 105 |
| 59 | 104 | | ]; |
| 60 | 105 | | } |