Деньги: целые копейки
Цена, скидка, итог к оплате — числа, в которых нельзя потерять копейку: на складе тысячи заказов в день, и копейки складываются в рубли расхождений со счётом в банке. Дальше — почему деньги не хранят в float64.
Попробуйте сейчас: что выведет?
Цель: предсказать сумму двух дробных переменных и отправить ответ.
Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func main() { a := 0.1 b := 0.7 fmt.Println(a + b) }▶ Выполните · выделенное замените своимcourse answer day03.q4 ВАШ_ОТВЕТВместо
ВАШ_ОТВЕТ— строка вывода целиком.Отправьте ответ, потом проверьте запуском — программу нужно набрать в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { a := 0.1 b := 0.7 fmt.Println(a + b) }▶ Выполнитеcd ~/gocourse/day03/scratch go run .Разошлось — отправьте ответ заново, засчитывается последний.
Готово, когда: ответ отправлен — пункт проверки
q4.
Почему дробные числа неточны
Одна треть десятичной дробью — 0.3333…, цифры не кончаются, запись приблизительная. float64 хранит число в двоичной записи, и там «бесконечны» привычные 0.1, 0.2, 0.29: в памяти лежит ближайшее двоичное число, а сумма двух приближений расходится с ожидаемой в шестнадцатом знаке, как в предсказании выше.
При этом fmt.Println(0.1 + 0.7) — числа прямо в коде — печатает 0.8: это константы, компилятор складывает их точно. Неточность начинается с переменных.
Ситуация. Товар за 19.99 руб., три штуки, и цена 29 копеек.
price := 19.99
qty := 3
fmt.Println(price * float64(qty))
kop := 0.29
fmt.Println(int(kop * 100))59.97
28Первая строка верна. Во второй 0.29 рубля перевели в копейки — вышло 28: в памяти лежало чуть меньше 0.29, после умножения — чуть меньше 29, а int отбросил «дробь» 0.999… Копейка пропала без ошибки, и округление при печати её не возвращает:
total := 0.0
total = total + 0.1
total = total + 0.1
total = total + 0.1
fmt.Println(total)
fmt.Printf("%.2f\n", total)0.30000000000000004
0.30%.2f показал 0.30, а в переменной осталось неточное число, с которым и продолжат следующие расчёты.
Правило: деньги — в копейках, в int
Целые числа хранятся точно: 1999 копеек — ровно 1999, в любом количестве сложений и умножений.
price := 1999
qty := 3
total := price * qty
fmt.Printf("%d руб. %02d коп.\n", total/100, total%100)59 руб. 97 коп.Рубли появляются только при печати — делением и остатком со второй страницы, копейки — через %02d с четвёртой. Дробная копейка при процентах и делении отбрасывается: так делает целочисленное деление, если условие не требует другого.
Где float64 всё-таки появляется. Когда сумму надо показать, округлив: в дне 04 цена станет рублями записью math.Round(float64(priceKop) / 100). Это вторая половина правила: считаем в копейках целыми, float64 берём только на последнем шаге, для показа, и обратно в расчёты его не возвращаем. Запрещено показанное выше: хранить цену как 19.99, складывать суммы в float64, переводить рубли в копейки умножением на 100.
Отсюда и задание avg. Средняя цена — величина не для учёта, а для показа: её считают в float64 до деления, и %.2f округляет её до копейки. Поделите в int, а float64 возьмёте после деления — средняя выйдет на копейку меньше: дробь уже потеряна.
Скидка в процентах: сначала умножить
Ситуация. Заказ на 5997 копеек, скидка 15 %: умножить на 15 и разделить на 100. Три записи:
total := 5997
pct := 15
fmt.Println(total*pct/100, total/100*pct, pct/100*total)899 885 0Как это читать.
| Выражение | Шаги | Итог |
|---|---|---|
total*pct/100 |
5997*15 = 89955, 89955/100 = 899 — отброшено 0.55 копейки |
899 — верно |
total/100*pct |
5997/100 = 59 — отброшено 97 коп., 59*15 |
885 — потеряно 14 копеек |
pct/100*total |
15/100 = 0 — отброшено всё, 0*5997 |
0 — скидки нет |
Математически выражения одинаковы, в целых числах — нет: деление отбрасывает дробь, и чем раньше оно выполнено, тем больше потеряно. Сначала умножить, потом делить — потеря не больше копейки. Итог к оплате — сумма минус скидка:
off := sum * discount / 100
total := sum - offПроценты: та же ловушка
Ситуация. Собрано 3 заказа из 4, нужна готовность в процентах.
packed := 3
total := 4
percent := packed / total * 100
fmt.Printf("Готовность: %d%%\n", percent)
fmt.Printf("Готовность: %d%%\n", packed*100/total)Готовность: 0%
Готовность: 75%3 / 4 — это 0.75, дробь отброшена: 0, и 0 * 100 = 0. Программа собирается, go vet молчит, а выводит неправду — такое видно только по выводу, сверенному с расчётом на бумаге. Через float64 и int(...) результат иногда выходит на единицу меньше (та же история, что с 0.29), поэтому для процентов — целые числа и «умножить, потом разделить».
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
0.30000000000000004 вместо 0.3 |
двоичное приближение дробей | деньги — в копейках, в int |
| средняя на копейку меньше | делили в int, float64 взяли после деления |
float64(sum) / float64(count) |
процент или скидка — 0 |
деление раньше умножения: 3/4 уже 0 |
packed * 100 / total |
| скидка на копейки меньше | делили на 100 до умножения | sum * discount / 100 |
Итого: 50 руб. 5 коп. или %! вместо % |
копейки через %d; одиночный % в шаблоне |
%02d и %% |
Попробуйте сейчас: задание
order.Цель:
order/main.goпечатает сумму, скидку и итог поTASK.txt, аgo vet .молчит.
- Прочитайте условие — цена, количество и скидка у вас свои:
▶ Выполнитеcd ~/gocourse/day03/order cat TASK.txt
- Все деньги —
intв копейках; печать — триPrintf, копейки через%02d, процент —%%.- Посчитайте свой итог на бумаге, потом запустите и сравните:
▶ Выполнитеgo run . go vet .Готово, когда: итог совпал с посчитанным на бумаге и с
TASK.txt, аgo vet .ничего не напечатал — пунктыorderиvet.
Попробуйте сейчас: почините программу
fix2.Цель:
fix2печатает правильную готовность в процентах, правка — одна строка безfloat64.
- Программа
fix2собирается и печатает неправду — запустите её:▶ Выполнитеcd ~/gocourse/day03/fix2 go run .
- Что должна напечатать — в комментарии в начале файла. Найдите строку, где дробь теряется, и почините её, не добавляя
float64: проверка смотрит и на это. Хватает одной строки, принимается не больше двух. Копию программы проверка запустит с другими числами: вписать готовый процент не выйдет.Готово, когда:
go run .печатает обещанное в комментарии, изменено не больше двух строк иfloat64не появился — пунктfix2.
Дальше: деление на ноль и переполнение — course next