言語ごとの結果
正解表 15件のうち、正解と一致した件数です。
PHPPHP 8.4.26
よくある書き方10 / 15
直した書き方15 / 15
PythonPython 3.14.7
よくある書き方6 / 15
直した書き方15 / 15
GoGo 1.27.2
よくある書き方10 / 15
直した書き方15 / 15
JavaScriptNode 24.15.0
よくある書き方10 / 15
直した書き方15 / 15
正解表と、よくある書き方の答え
正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。
| ケース | 入力 | 正解 | PHP | Python | Go | JavaScript |
|---|---|---|---|---|---|---|
| c001基本・3000円の20%引き 元ツールの初期値 |
|
|
✓ | ✓ | ✓ | ✓ |
| c002基本・税込10% |
|
|
✓ | ✓ | ✓ | ✓ |
| c00330%引きで .5 ちょうど 45 × 0.7 = 31.5 → 32。浮動小数点では 31.499… になり 31 |
|
|
discounted31off14with_tax31 | discounted31off14with_tax31 | discounted31off14with_tax31 | discounted31off14with_tax31 |
| c00430%引きで .5 ちょうど(その2) 85 × 0.7 = 59.5 → 60。浮動小数点では 59.499… |
|
|
discounted59off26with_tax59 | discounted59off26with_tax59 | discounted59off26with_tax59 | discounted59off26with_tax59 |
| c00533%引きで .5 ちょうど 150 × 0.67 = 100.5 → 101。浮動小数点では 100.499… |
|
|
discounted100off50with_tax100 | discounted100off50with_tax100 | discounted100off50with_tax100 | discounted100off50with_tax100 |
| c00650%引きで .5 ちょうど(2進で正確) 502.5 → 503。偶数丸めだと 502。割引額は 1005 − 503 = 502(元ツールは 503 と出す) |
|
|
✓ | discounted502off503with_tax502 | ✓ | ✓ |
| c00730%引きで 10.5 10.5 → 11。偶数丸めだと 10 |
|
|
✓ | discounted10off5with_tax10 | ✓ | ✓ |
| c00890%引きで 0.5 5 × 0.1 = 0.5 → 1。浮動小数点では 0.4999… になり 0 |
|
|
discounted0off5with_tax0 | discounted0off5with_tax0 | discounted0off5with_tax0 | discounted0off5with_tax0 |
| c009割引率0% 境界値 |
|
|
✓ | ✓ | ✓ | ✓ |
| c010割引率100% 境界値。税込も 0 |
|
|
✓ | ✓ | ✓ | ✓ |
| c011小数の割引率 666.999… ではなく 667 |
|
|
✓ | ✓ | ✓ | ✓ |
| c012小数の割引率で .5 ちょうど 1500 × 0.667 = 1000.5 → 1001。偶数丸めだと 1000 |
|
|
✓ | discounted1000off500with_tax1000 | ✓ | ✓ |
| c013税込で .5 ちょうど 15 × 1.1 = 16.5 → 17。偶数丸めだと 16 |
|
|
✓ | with_tax16 | ✓ | ✓ |
| c014軽減税率8% 1048.9 → 1049、1049 × 1.08 = 1132.92 → 1133 |
|
|
✓ | ✓ | ✓ | ✓ |
| c0152^53 を超える価格 倍精度浮動小数点で正確に表せない整数(2^53+1)。半額は 4503599627370496.5 → 4503599627370497 |
|
|
discounted4503599627370496off4503599627370497with_tax4503599627370496 | discounted4503599627370496off4503599627370497with_tax4503599627370496 | discounted4503599627370496off4503599627370497with_tax4503599627370496 | discounted4503599627370496with_tax4503599627370496 |
コード
「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。
Discount.php+13 −4よくある書き方(04ec732)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | <?php | |
| 2 | 2 | declare(strict_types=1); | |
| 3 | 3 | ||
| 4 | + | use BcMath\Number; | |
| 5 | + | ||
| 4 | 6 | /** | |
| 5 | 7 | * 割引後の価格と税込。 | |
| 6 | − | * v1: 素直に書いた版(float と round)。 | |
| 7 | − | * @return array{discounted:int, off:int, with_tax:int} | |
| 8 | + | * v2: 十進(BcMath\Number)で計算し、四捨五入は RoundingMode::HalfAwayFromZero で明示する。 | |
| 9 | + | * (float と round だと 45 × 0.7 が 31.499… になるうえ、PHP 8.4.4 の round は 2^52 以上の偶数を +1 する) | |
| 8 | 10 | */ | |
| 11 | + | function roundYen(Number $x): int | |
| 12 | + | { | |
| 13 | + | return (int) $x->round(0, RoundingMode::HalfAwayFromZero)->value; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | /** @return array{discounted:int, off:int, with_tax:int} */ | |
| 9 | 17 | function calcDiscount(int $price, string $rate, string $tax): array | |
| 10 | 18 | { | |
| 11 | − | $discounted = (int) round($price * (1 - (float) $rate / 100)); | |
| 19 | + | $hundred = new Number(100); | |
| 20 | + | $discounted = roundYen(new Number($price) * ($hundred - new Number($rate)) / $hundred); | |
| 12 | 21 | return [ | |
| 13 | 22 | 'discounted' => $discounted, | |
| 14 | 23 | 'off' => $price - $discounted, | |
| 15 | − | 'with_tax' => (int) round($discounted * (1 + (float) $tax / 100)), | |
| 24 | + | 'with_tax' => roundYen(new Number($discounted) * ($hundred + new Number($tax)) / $hundred), | |
| 16 | 25 | ]; | |
| 17 | 26 | } |
Discount.php
<?php
declare(strict_types=1);
use BcMath\Number;
/**
* 割引後の価格と税込。
* v2: 十進(BcMath\Number)で計算し、四捨五入は RoundingMode::HalfAwayFromZero で明示する。
* (float と round だと 45 × 0.7 が 31.499… になるうえ、PHP 8.4.4 の round は 2^52 以上の偶数を +1 する)
*/
function roundYen(Number $x): int
{
return (int) $x->round(0, RoundingMode::HalfAwayFromZero)->value;
}
/** @return array{discounted:int, off:int, with_tax:int} */
function calcDiscount(int $price, string $rate, string $tax): array
{
$hundred = new Number(100);
$discounted = roundYen(new Number($price) * ($hundred - new Number($rate)) / $hundred);
return [
'discounted' => $discounted,
'off' => $price - $discounted,
'with_tax' => roundYen(new Number($discounted) * ($hundred + new Number($tax)) / $hundred),
];
}
Discount.phpコミット 04ec732
<?php
declare(strict_types=1);
/**
* 割引後の価格と税込。
* v1: 素直に書いた版(float と round)。
* @return array{discounted:int, off:int, with_tax:int}
*/
function calcDiscount(int $price, string $rate, string $tax): array
{
$discounted = (int) round($price * (1 - (float) $rate / 100));
return [
'discounted' => $discounted,
'off' => $price - $discounted,
'with_tax' => (int) round($discounted * (1 + (float) $tax / 100)),
];
}
discount.py+9 −3よくある書き方(04ec732)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | """割引後の価格と税込。 | |
| 2 | 2 | ||
| 3 | − | v1: 素直に書いた版(float と round)。 | |
| 3 | + | v2: decimal で計算し、四捨五入は ROUND_HALF_UP で明示する | |
| 4 | + | (float と round だと 45 × 0.7 が 31.499… になり、round は偶数丸め)。 | |
| 4 | 5 | """ | |
| 6 | + | from decimal import ROUND_HALF_UP, Decimal | |
| 7 | + | ||
| 8 | + | ||
| 9 | + | def round_yen(x: Decimal) -> int: | |
| 10 | + | return int(x.quantize(Decimal(1), rounding=ROUND_HALF_UP)) | |
| 5 | 11 | ||
| 6 | 12 | ||
| 7 | 13 | def calc_discount(price: int, rate: str, tax: str) -> dict: | |
| 8 | − | discounted = round(price * (1 - float(rate) / 100)) | |
| 9 | − | return {"discounted": discounted, "off": price - discounted, "with_tax": round(discounted * (1 + float(tax) / 100))} | |
| 14 | + | discounted = round_yen(price * (100 - Decimal(rate)) / 100) | |
| 15 | + | return {"discounted": discounted, "off": price - discounted, "with_tax": round_yen(discounted * (100 + Decimal(tax)) / 100)} |
discount.py
"""割引後の価格と税込。
v2: decimal で計算し、四捨五入は ROUND_HALF_UP で明示する
(float と round だと 45 × 0.7 が 31.499… になり、round は偶数丸め)。
"""
from decimal import ROUND_HALF_UP, Decimal
def round_yen(x: Decimal) -> int:
return int(x.quantize(Decimal(1), rounding=ROUND_HALF_UP))
def calc_discount(price: int, rate: str, tax: str) -> dict:
discounted = round_yen(price * (100 - Decimal(rate)) / 100)
return {"discounted": discounted, "off": price - discounted, "with_tax": round_yen(discounted * (100 + Decimal(tax)) / 100)}
discount.pyコミット 04ec732
"""割引後の価格と税込。
v1: 素直に書いた版(float と round)。
"""
def calc_discount(price: int, rate: str, tax: str) -> dict:
discounted = round(price * (1 - float(rate) / 100))
return {"discounted": discounted, "off": price - discounted, "with_tax": round(discounted * (1 + float(tax) / 100))}
discount.go+35 −7よくある書き方(04ec732)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | package main | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | − | "math" | |
| 5 | − | "strconv" | |
| 4 | + | "fmt" | |
| 5 | + | "math/big" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | 8 | // Output は割引後・割引額・税込(円)。 | |
| @@ 変更前 12行目〜 / 変更後 12行目〜 @@ | |||
| 12 | 12 | WithTax int64 `json:"with_tax"` | |
| 13 | 13 | } | |
| 14 | 14 | ||
| 15 | + | // v2: math/big.Rat で正確に計算し、四捨五入は自前で行う | |
| 16 | + | // (math.Round は float64 の 31.499… を 31 にする。big.Rat には丸めモード付きの整数化が無い)。 | |
| 17 | + | func roundHalfUp(x *big.Rat) (int64, error) { | |
| 18 | + | v := new(big.Rat).Add(x, big.NewRat(1, 2)) // x >= 0 | |
| 19 | + | n := new(big.Int).Quo(v.Num(), v.Denom()) // 0 以上なので Quo は floor | |
| 20 | + | if !n.IsInt64() { | |
| 21 | + | return 0, fmt.Errorf("out of int64 range: %s", n) | |
| 22 | + | } | |
| 23 | + | return n.Int64(), nil | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // times は x × (100 ± pct) / 100 を返す。 | |
| 27 | + | func times(x int64, pct string, sign int64) (*big.Rat, error) { | |
| 28 | + | p, ok := new(big.Rat).SetString(pct) | |
| 29 | + | if !ok { | |
| 30 | + | return nil, fmt.Errorf("invalid decimal: %q", pct) | |
| 31 | + | } | |
| 32 | + | f := new(big.Rat).Add(big.NewRat(100, 1), p.Mul(p, big.NewRat(sign, 1))) | |
| 33 | + | f.Quo(f, big.NewRat(100, 1)) | |
| 34 | + | return f.Mul(f, new(big.Rat).SetInt64(x)), nil | |
| 35 | + | } | |
| 36 | + | ||
| 15 | 37 | // CalcDiscount は割引後の価格と税込を求める。 | |
| 16 | − | // v1: 素直に書いた版(float64 と math.Round)。 | |
| 17 | 38 | func CalcDiscount(price int64, rate, tax string) (Output, error) { | |
| 18 | − | r, err := strconv.ParseFloat(rate, 64) | |
| 39 | + | d, err := times(price, rate, -1) | |
| 19 | 40 | if err != nil { | |
| 20 | 41 | return Output{}, err | |
| 21 | 42 | } | |
| 22 | − | t, err := strconv.ParseFloat(tax, 64) | |
| 43 | + | discounted, err := roundHalfUp(d) | |
| 23 | 44 | if err != nil { | |
| 24 | 45 | return Output{}, err | |
| 25 | 46 | } | |
| 26 | − | discounted := int64(math.Round(float64(price) * (1 - r/100))) | |
| 27 | − | return Output{discounted, price - discounted, int64(math.Round(float64(discounted) * (1 + t/100)))}, nil | |
| 47 | + | w, err := times(discounted, tax, 1) | |
| 48 | + | if err != nil { | |
| 49 | + | return Output{}, err | |
| 50 | + | } | |
| 51 | + | withTax, err := roundHalfUp(w) | |
| 52 | + | if err != nil { | |
| 53 | + | return Output{}, err | |
| 54 | + | } | |
| 55 | + | return Output{discounted, price - discounted, withTax}, nil | |
| 28 | 56 | } |
discount.go
package main
import (
"fmt"
"math/big"
)
// Output は割引後・割引額・税込(円)。
type Output struct {
Discounted int64 `json:"discounted"`
Off int64 `json:"off"`
WithTax int64 `json:"with_tax"`
}
// v2: math/big.Rat で正確に計算し、四捨五入は自前で行う
// (math.Round は float64 の 31.499… を 31 にする。big.Rat には丸めモード付きの整数化が無い)。
func roundHalfUp(x *big.Rat) (int64, error) {
v := new(big.Rat).Add(x, big.NewRat(1, 2)) // x >= 0
n := new(big.Int).Quo(v.Num(), v.Denom()) // 0 以上なので Quo は floor
if !n.IsInt64() {
return 0, fmt.Errorf("out of int64 range: %s", n)
}
return n.Int64(), nil
}
// times は x × (100 ± pct) / 100 を返す。
func times(x int64, pct string, sign int64) (*big.Rat, error) {
p, ok := new(big.Rat).SetString(pct)
if !ok {
return nil, fmt.Errorf("invalid decimal: %q", pct)
}
f := new(big.Rat).Add(big.NewRat(100, 1), p.Mul(p, big.NewRat(sign, 1)))
f.Quo(f, big.NewRat(100, 1))
return f.Mul(f, new(big.Rat).SetInt64(x)), nil
}
// CalcDiscount は割引後の価格と税込を求める。
func CalcDiscount(price int64, rate, tax string) (Output, error) {
d, err := times(price, rate, -1)
if err != nil {
return Output{}, err
}
discounted, err := roundHalfUp(d)
if err != nil {
return Output{}, err
}
w, err := times(discounted, tax, 1)
if err != nil {
return Output{}, err
}
withTax, err := roundHalfUp(w)
if err != nil {
return Output{}, err
}
return Output{discounted, price - discounted, withTax}, nil
}
discount.goコミット 04ec732
package main
import (
"math"
"strconv"
)
// Output は割引後・割引額・税込(円)。
type Output struct {
Discounted int64 `json:"discounted"`
Off int64 `json:"off"`
WithTax int64 `json:"with_tax"`
}
// CalcDiscount は割引後の価格と税込を求める。
// v1: 素直に書いた版(float64 と math.Round)。
func CalcDiscount(price int64, rate, tax string) (Output, error) {
r, err := strconv.ParseFloat(rate, 64)
if err != nil {
return Output{}, err
}
t, err := strconv.ParseFloat(tax, 64)
if err != nil {
return Output{}, err
}
discounted := int64(math.Round(float64(price) * (1 - r/100)))
return Output{discounted, price - discounted, int64(math.Round(float64(discounted) * (1 + t/100)))}, nil
}
discount.mjs+22 −3よくある書き方(04ec732)→ 直した書き方
| @@ 変更前 1行目〜 / 変更後 1行目〜 @@ | |||
| 1 | 1 | // 割引後の価格と税込。 | |
| 2 | − | // v1: 素直に書いた版(Number と Math.round)。 | |
| 2 | + | // v2: BigInt の分数で正確に計算し、四捨五入も自前で行う(Math.round は float の 31.499… を 31 にする)。 | |
| 3 | + | // price は BigInt で受け取り、BigInt で返す(2^53 を超える価格のため)。 | |
| 4 | + | ||
| 5 | + | /** "33.3" → { n: 333n, d: 10n } */ | |
| 6 | + | function parseDecimal(s) { | |
| 7 | + | const m = /^(\d+)(?:\.(\d+))?$/.exec(s); | |
| 8 | + | if (!m) throw new RangeError(`invalid decimal: ${s}`); | |
| 9 | + | const frac = m[2] ?? ""; | |
| 10 | + | return { n: BigInt(m[1] + frac), d: 10n ** BigInt(frac.length) }; | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | /** 0 以上の num/den を四捨五入した BigInt */ | |
| 14 | + | const roundHalfUp = (num, den) => (2n * num + den) / (2n * den); | |
| 3 | 15 | ||
| 4 | 16 | export function calcDiscount({ price, rate, tax }) { | |
| 5 | − | const discounted = Math.round(price * (1 - Number(rate) / 100)); | |
| 6 | − | return { discounted, off: price - discounted, with_tax: Math.round(discounted * (1 + Number(tax) / 100)) }; | |
| 17 | + | const r = parseDecimal(rate); | |
| 18 | + | const t = parseDecimal(tax); | |
| 19 | + | // price × (100 − rate) / 100 = price × (100·r.d − r.n) / (100·r.d) | |
| 20 | + | const discounted = roundHalfUp(price * (100n * r.d - r.n), 100n * r.d); | |
| 21 | + | return { | |
| 22 | + | discounted, | |
| 23 | + | off: price - discounted, | |
| 24 | + | with_tax: roundHalfUp(discounted * (100n * t.d + t.n), 100n * t.d), | |
| 25 | + | }; | |
| 7 | 26 | } |
discount.mjs
// 割引後の価格と税込。
// v2: BigInt の分数で正確に計算し、四捨五入も自前で行う(Math.round は float の 31.499… を 31 にする)。
// price は BigInt で受け取り、BigInt で返す(2^53 を超える価格のため)。
/** "33.3" → { n: 333n, d: 10n } */
function parseDecimal(s) {
const m = /^(\d+)(?:\.(\d+))?$/.exec(s);
if (!m) throw new RangeError(`invalid decimal: ${s}`);
const frac = m[2] ?? "";
return { n: BigInt(m[1] + frac), d: 10n ** BigInt(frac.length) };
}
/** 0 以上の num/den を四捨五入した BigInt */
const roundHalfUp = (num, den) => (2n * num + den) / (2n * den);
export function calcDiscount({ price, rate, tax }) {
const r = parseDecimal(rate);
const t = parseDecimal(tax);
// price × (100 − rate) / 100 = price × (100·r.d − r.n) / (100·r.d)
const discounted = roundHalfUp(price * (100n * r.d - r.n), 100n * r.d);
return {
discounted,
off: price - discounted,
with_tax: roundHalfUp(discounted * (100n * t.d + t.n), 100n * t.d),
};
}
discount.mjsコミット 04ec732
// 割引後の価格と税込。
// v1: 素直に書いた版(Number と Math.round)。
export function calcDiscount({ price, rate, tax }) {
const discounted = Math.round(price * (1 - Number(rate) / 100));
return { discounted, off: price - discounted, with_tax: Math.round(discounted * (1 + Number(tax) / 100)) };
}
仕様
入力
| 項目 | 型 | 説明 |
|---|---|---|
price | 整数(円) | 元の価格。0 以上 |
rate | 十進文字列 | 割引率(%)。0 以上 100 以下。"33.3" のような小数を含む |
tax | 十進文字列 | 税率(%)。"0"(税込を出さない)・"8"・"10" |
出力
| 項目 | 型 | 定義 |
|---|---|---|
discounted | 整数(円) | 四捨五入(price × (1 − rate/100)) |
off | 整数(円) | price − discounted(割引額) |
with_tax | 整数(円) | 四捨五入(discounted × (1 + tax/100))。tax = "0" なら discounted と同じ |
- 計算は正確な十進(または有理数)で行う。 2進浮動小数点の誤差を結果に出さない
- 四捨五入は 0.5 ちょうどを繰り上げる(偶数丸めではない)。値はすべて 0 以上
priceは 64bit 符号付き整数に収まる範囲(c015は 2^53 を超える値)
元ツールとの違い(2026-10-10 に決めた)
元ツールは次のように計算している。
discounted = price × (1 − rate/100) … 丸めずに保持し、表示時に Math.round off = price − discounted … 同上 withTax = discounted × (1 + tax/100) … 丸める前の discounted に税をかけ、表示時に Math.round
本 spec はこれを2点変えた。
- 割引額は「元の価格 − 丸めた割引後の価格」にする。 元ツールは割引後と割引額を別々に丸めるため、 1,005円の50%引きで「割引後 503円・割引額 503円」となり、足すと元の価格を1円超える
- 税は丸めた割引後の価格(実際に払う税抜の金額)にかける。 元ツールは丸める前の値にかけている
この2点は浮動小数点の誤差とは別の問題(定義の問題)なので、元ツールに対する指摘として別に扱う。