День 03 · Типы и арифметика · страница 5 из 6

Деньги: целые копейки

Цена, скидка, итог к оплате — числа, в которых нельзя потерять копейку: на складе тысячи заказов в день, и копейки складываются в рубли расхождений со счётом в банке. Дальше — почему деньги не хранят в 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.go
package 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 . молчит.

  1. Прочитайте условие — цена, количество и скидка у вас свои:
▶ Выполните
cd ~/gocourse/day03/order
cat TASK.txt
  1. Все деньги — int в копейках; печать — три Printf, копейки через %02d, процент — %%.
  2. Посчитайте свой итог на бумаге, потом запустите и сравните:
▶ Выполните
go run .
go vet .

Готово, когда: итог совпал с посчитанным на бумаге и с TASK.txt, а go vet . ничего не напечатал — пункты order и vet.


Попробуйте сейчас: почините программу fix2.

Цель: fix2 печатает правильную готовность в процентах, правка — одна строка без float64.

  1. Программа fix2 собирается и печатает неправду — запустите её:
▶ Выполните
cd ~/gocourse/day03/fix2
go run .
  1. Что должна напечатать — в комментарии в начале файла. Найдите строку, где дробь теряется, и почините её, не добавляя float64: проверка смотрит и на это. Хватает одной строки, принимается не больше двух. Копию программы проверка запустит с другими числами: вписать готовый процент не выйдет.

Готово, когда: go run . печатает обещанное в комментарии, изменено не больше двух строк и float64 не появился — пункт fix2.


Дальше: деление на ноль и переполнение — course next