День 05 · Контрольная точка 1: как решать задачу · страница 2 из 5

Пишем по кусочкам

Ситуация. Для задачи об упаковке заказа шаги 1–3 сделаны: условие прочитано, примеры посчитаны, подцели названы. Нужна программа, которая на всех трёх примерах печатает то же, что таблица. Написать её целиком и запустить один раз — значит, получить неверное число и не знать, в какой из десяти строк ошибка. Поэтому шаг 4 — код кусками. Правило одно: пара строк — запуск — сверка с таблицей примеров.

Эту страницу достаточно прочитать: весь разбор с настоящим выводом — ниже, а приглашение ~/gocourse/day05/pack$ в примерах говорит, где он снят. Повторить разбор руками — задание со звёздочкой в конце страницы: тогда программа целиком живёт в папке ~/gocourse/day05/pack, и проверка её не сверяет.


Кусок 1: данные

Начинаем с переменных из условия и первой строки вывода:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	items := 50
	perBox := 12
	boxPrice := 90

	fmt.Println("Штук в заказе:", items)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
# day05/pack
./main.go:7:2: declared and not used: perBox
./main.go:8:2: declared and not used: boxPrice

Первая ловушка метода «кусками» — именно в Go: компилятор не собирает программу с переменной, которая нигде не используется (день 02). В законченной программе это полезно — лишняя переменная обычно означает забытый расчёт. В недописанной мешает.

Выхода два. Объявлять переменную, когда до неё дошла очередь. Или временно напечатать все данные отдельной строкой — заодно видно, что они такие, как задумано.

Дальше показываем только строки из тела main — вставляйте их между { и } в scratch/main.go. С одним import "fmt" первая строка тела — шестая. В разборе ниже их место — в конце main программы из куска 1, перед }. То есть отдельный фрагмент пробуют в scratch/main.go, а программу целиком, если повторяете разбор руками, держат в pack.

Код для чтения · разбираем, набирать не нужно
	fmt.Println("данные:", items, perBox, boxPrice)
	fmt.Println("Штук в заказе:", items)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 50 12 90
Штук в заказе: 50

Строка данные: временная, в конце её уберём.


Кусок 2: коробки

Первая мысль — поделить:

Код для чтения · разбираем, набирать не нужно
	boxes := items / perBox
	fmt.Println("Коробок:", boxes)
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 50 12 90
Штук в заказе: 50
Коробок: 4

Сверка с таблицей: нужно 5. Деление целых отбрасывает остаток (день 03), и две штуки остались без коробки. Добавим коробку:

Код для чтения · разбираем, набирать не нужно
	boxes := items/perBox + 1
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 50 12 90
Штук в заказе: 50
Коробок: 5

С первым примером совпало, и здесь обычно останавливаются. Меняем items := 50 на items := 48 — второй пример:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 48 12 90
Штук в заказе: 48
Коробок: 5

По таблице нужно 4. + 1 добавляет коробку всегда, даже когда остатка нет. Без неудобного примера эта ошибка дожила бы до склада.

Нужна запись «разделить и округлить вверх» на целых числах:

Код для чтения · разбираем, набирать не нужно
	boxes := (items + perBox - 1) / perBox

Как это читать. К числу штук добавляем почти полную коробку — на одну штуку меньше полной. Если остаток был, сумма переходит через границу следующей коробки, и деление даёт на одну больше. Если остатка не было, perBox - 1 до следующей границы не дотягивает, и результат прежний:

items items + perBox − 1 ÷ 12, дробь отброшена
50 61 5
48 59 4
7 18 1
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 48 12 90
Штук в заказе: 48
Коробок: 4

С items := 7:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 7 12 90
Штук в заказе: 7
Коробок: 1

Тот же результат дала бы math.Ceil из дня 04, через дробные числа: int(math.Ceil(float64(items) / float64(perBox))) и import "math". Целочисленная запись короче и обходится без дробей.


Кусок 3: последняя коробка

Снова первая мысль — остаток от деления:

Код для чтения · разбираем, набирать не нужно
	last := items % perBox
	fmt.Println("В последней коробке:", last, "шт.")

С items := 48:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 48 12 90
Штук в заказе: 48
Коробок: 4
В последней коробке: 0 шт.

Коробок с нулём штук не бывает: по таблице в последней 12. Остаток годится, только когда он есть. Считаем иначе: все коробки, кроме последней, полные, а последней достаётся то, что осталось:

Код для чтения · разбираем, набирать не нужно
	last := items - (boxes-1)*perBox
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
данные: 48 12 90
Штук в заказе: 48
Коробок: 4
В последней коробке: 12 шт.

Для 50: 50 − 4 × 12 = 2. Для 7: 7 − 0 × 12 = 7. Совпадает со всей таблицей.


Кусок 4: стоимость и итог

Стоимость — в копейках, печать в рублях через %d.%02d, как в день 03. Временную строку данные: убираем, комментарии дописываем. Вся программа:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	items := 50    // штук в заказе
	perBox := 12   // штук в одной коробке
	boxPrice := 90 // упаковка одной коробки, копеек

	boxes := (items + perBox - 1) / perBox
	last := items - (boxes-1)*perBox
	cost := boxes * boxPrice

	fmt.Println("Штук в заказе:", items)
	fmt.Println("Коробок:", boxes)
	fmt.Println("В последней коробке:", last, "шт.")
	fmt.Printf("Упаковка: %d.%02d руб.\n", cost/100, cost%100)
}

Как это читать. Три части, разделённые пустыми строками: данные, расчёт, печать. Числа из условия записаны один раз — в начале. Расчёт и печать пользуются только именами. Поэтому проверить другой пример — значит поменять одно число в первой строке, и больше ничего.


Шаг 5: сверить

Примеры по очереди — 50, 48 и 7:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
Штук в заказе: 50
Коробок: 5
В последней коробке: 2 шт.
Упаковка: 4.50 руб.
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
Штук в заказе: 48
Коробок: 4
В последней коробке: 12 шт.
Упаковка: 3.60 руб.
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ go run .
Штук в заказе: 7
Коробок: 1
В последней коробке: 7 шт.
Упаковка: 0.90 руб.

Каждая строка совпала с таблицей. Осталось проверить формат и подозрительные места:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day05/pack$ gofmt -l .
stagiaire@lab:~/gocourse/day05/pack$ go vet .

Обе команды молчат — это успех. go vet ловит то, что пропускает компилятор, например %d для дробного числа в Printf.

И последнее: верните в первой строке значение из условия. Сдаётся программа с данными условия, а не с последним примером, который вы пробовали.


Что может пойти не так

Что видите Что это значит Что делать
declared and not used: perBox на первом запуске переменная объявлена раньше, чем понадобилась временная строка fmt.Println("данные:", ...) или объявлять позже
с одним примером совпало, с другим нет формула верна только для удобных данных не подгонять под пример, а понять, чем примеры отличаются: есть ли остаток, не ноль ли
Упаковка: 1.5 руб. при стоимости 105 копеек копейки напечатаны %d, а не %02d %d.%02d
после правки печатается старое файл не сохранён Ctrl+S
в выводе осталась строка данные: ... временная печать не убрана удалить перед сдачей
числа из условия вписаны прямо в Println программа верна только для одного набора данных числа — один раз в переменных в начале main, дальше только имена

Попробуйте сейчас: разминка warmup.

Цель: warmup/main.go решает задачу из TASK.txt и проходит оба запуска проверки.

Первая задача, которую сверит course check 05. В контрольную точку она не входит, но устроена так же, как её задачи на написание программ.

▶ Выполните
cd ~/gocourse/day05/warmup
cat TASK.txt

В условии — пример с ответом и ваши данные без ответа. Пройдите пять шагов: посчитайте ответ для своих данных на бумаге, назовите подцели, пишите main.go кусками. Файла main.go в папке нет — создайте его.

Первые строки main — ровно такие, как в разделе «ВАШИ ДАННЫЕ». Проверка запускает программу дважды: как есть и ещё раз, подставив в эти строки данные примера. Поэтому правило «числа из условия — один раз, в начале» здесь обязательное: число, посчитанное в уме и вписанное в Println, второй запуск не пройдёт.

Когда вывод совпал с посчитанным на бумаге — шаг 5, сверка. Все три команды проверка ищет в журнале:

▶ Выполните
go run .
gofmt -l .
go vet .

Готово, когда: вывод go run . совпал с ответом, посчитанным на бумаге для ваших данных, а gofmt -l . и go vet . ничего не напечатали (пункты проверки warmup, t_gorun, t_gofmt, t_vet).


Со звёздочкой, в зачёт не входит. Повторите разбор в ~/gocourse/day05/pack — кусками, с запусками, как на странице. Прогоните четыре примера: три из таблицы и 25 штук с прошлой страницы — совпал ли ваш ответ на бумаге?


Дальше: как читать чужую программу — course next