День 19 · дополнительные материалы

День 19 — глубже

Всё здесь необязательно. Материалы на английском помечены.


Документация и статья (англ.)

То же открывается в терминале через go doc: go doc utf8, go doc strings.Fields, go doc os.ReadFile, go doc bufio.Scanner.

fmt считает буквы сам

Ширина в Printf — %-10s — считается в буквах, а не в байтах. Точность %.3s — сколько букв оставить.

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

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	a := "й"
	b := "и\u0306"
	fmt.Println(a, b, a == b)
	fmt.Println(len(a), utf8.RuneCountInString(a))
	fmt.Println(len(b), utf8.RuneCountInString(b))
	fmt.Printf("[%-10s]\n", "Болт")
	fmt.Printf("[%.3s]\n", "Болт М8")
	fmt.Printf("%q\n", strings.Split("TK-1;Болт\r\nTK-2;Шайба\r\n", "\n"))
	var sb strings.Builder
	sb.WriteString("Болт")
	sb.WriteString(strings.Repeat(".", 3))
	fmt.Println(sb.String(), sb.Len())
}
Вывод · это печатает программа, набирать не нужно
й й false
2 1
4 2
[Болт      ]
[Бол]
["TK-1;Болт\r" "TK-2;Шайба\r" ""]
Болт... 11

Ширину и точность можно передать аргументом — %-*s и %.*s. Тогда fitName обходится без utf8 и []rune: точность отрезает лишние буквы, а разница в байтах между строкой, добитой пробелами, и самим названием — ровно число недостающих букв.

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

import (
	"fmt"
	"strings"
)

func fitName(name string, width int) string {
	if fmt.Sprintf("%.*s", width, name) != name {
		return fmt.Sprintf("%.*s", width-1, name) + "~"
	}
	pad := fmt.Sprintf("%-*s", width, name)
	return name + strings.Repeat(".", len(pad)-len(name))
}

func main() {
	for _, name := range []string{"Болт М8", "Анкер клиновой", "Кронштейн усиленный"} {
		fmt.Println(fitName(name, 14) + "|")
	}
}
Вывод · это печатает программа, набирать не нужно
Болт М8.......|
Анкер клиновой|
Кронштейн уси~|

Такое решение задания засчитывается: колонка ровная по буквам. На страницах fitName написана через руны, чтобы было видно, что fmt делает внутри.

Одна буква — две руны

Первые три строки вывода первой программы. й можно записать одной руной (номер 1081) или двумя: и и отдельный знак «кратка» — в переменной b он записан как \u0306, так в строке Go записывают знак по его номеру. На экране одно и то же, а для Go это разные строки: a == b — false, у второй две руны и четыре байта. Такое приходит из файлов, созданных на других системах. Для отчётов дня это не важно; для поиска по названиям — важно, и решается пакетом golang.org/x/text/unicode/norm (не из стандартной библиотеки).

Выгрузки из Windows

Строка %q выше: файлы, сохранённые в Windows, кончают строки не \n, а \r\n. После Split по "\n" в конце каждой строки остаётся \r — невидимый в терминале. strings.TrimSpace его убирает, поэтому parseLine дня с такими файлами справляется. bufio.Scanner убирает \r сам.

strings.Builder

Последняя строка вывода выше. Когда строку собирают из многих кусков в цикле, + каждый раз создаёт новую строку. strings.Builder копит куски и отдаёт строку один раз — sb.String(). sb.Len() — длина в байтах: 11, а букв семь.

encoding/csv

В стандартной библиотеке есть пакет, который разбирает такие файлы сам: понимает поля в кавычках, внутри которых может стоять разделитель.

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

import (
	"encoding/csv"
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("orders.csv")
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}
	r := csv.NewReader(f)
	r.Comma = ';'
	rows, err := r.ReadAll()
	f.Close()
	fmt.Println(len(rows), err)
	for _, row := range rows {
		fmt.Println(len(row), row)
	}
}

Файл orders.csv для опыта — во второй строке название в кавычках с точкой с запятой внутри, третья строка битая:

Образец · набирать не нужно
TK-101;Болт М8;12;1500
TK-102;"Шайба; кровельная";100;90
TK-103;Гайка
Вывод · это печатает программа, набирать не нужно
0 record on line 3: wrong number of fields

ReadAll на первой битой строке останавливается и возвращает ошибку — ни одной строки не отдаёт. Чтобы идти дальше, как требует отчёт дня, читают по одной записи методом r.Read() в цикле и решают, что делать с ошибкой каждой. Разберите go doc csv.Reader.Read: когда он возвращает ошибку, а когда запись вместе с ошибкой.

Попробуйте сейчас: csv против Split.

Цель: увидеть, чем поле в кавычках отличается для encoding/csv и для strings.Split.

Создайте файл из трёх строк выше в scratch, наберите программу и запустите. Потом уберите третью строку и запустите ещё раз:

▶ Выполните
cd ~/gocourse/day19/scratch
go run .

Готово, когда: с битой строкой программа напечатала 0 и ошибку, без неё — две записи, и во второй поле Шайба; кровельная целиком. Проверка дня этот опыт не сверяет.