День 19 · Строки, руны и файлы · страница 3 из 6

Руны: буквы, а не байты

Байты — то, как строка лежит в памяти. Человеку нужны буквы: сколько их в названии, какая первая, где обрезать. Для этого в Go есть руна — одна буква (точнее, один знак Unicode), записанная числом.


range по строке идёт по буквам

Ситуация. Нужно пройти название по буквам. Цикл for i := 0; i < len(s); i++ идёт по байтам, а range из дня 16 со строкой работает иначе.

Код для чтения · разбираем, набирать не нужно
	for i, r := range "Болт М8" {
		fmt.Println(i, r, string(r))
	}

Что увидите.

Вывод · это печатает программа, набирать не нужно
0 1041 Б
2 1086 о
4 1083 л
6 1090 т
8 32  
9 1052 М
11 56 8

Как это читать. Семь проходов — по одному на букву, а не восемь и не двенадцать.

Переменная Что в ней
i номер байта, с которого начинается буква. Растёт на 2 после русской буквы и на 1 после пробела и цифры
r сама буква как число — руна. У Б номер 1041, у пробела 32. Тип — rune
string(r) руна обратно в строку из одной буквы: её можно склеивать с другими строками

range по строке сам разбирает UTF-8: даёт номер байта и букву, поэтому номера i идут с пропусками.


Попробуйте сейчас: что выведет range по строке.

Цель: предсказать номера i для строки из разных знаков и сдать ответ.

1. Не запуская, ответьте, какие числа окажутся в начале строк вывода:

Код для чтения · разбираем, набирать не нужно
	for i, r := range "Щ-7" {
		fmt.Println(i, string(r))
	}

2. Сдайте их через пробел:

▶ Выполните · выделенное замените своим
course answer day19.q3 ЧИСЛО ЧИСЛО ЧИСЛО

Вместо трёх ЧИСЛО — номера в том порядке, в каком их напечатает программа.

3. Проверьте запуском в scratch.

Готово, когда: course answer ответил Принято: day19.q3 = … (пункт проверки q3).


Длина в буквах и срез по буквам

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

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	name := "Болт М8"
	fmt.Println(len(name))
	fmt.Println(utf8.RuneCountInString(name))
	fmt.Println(len([]rune(name)))
	runes := []rune(name)
	fmt.Println(runes)
	fmt.Println(string(runes[:4]))
}

Что увидите.

Вывод · это печатает программа, набирать не нужно
12
7
7
[1041 1086 1083 1090 32 1052 56]
Болт

Как это читать.

Запись Что даёт Когда брать
len(name) 12 — байты размер в байтах: сколько места займёт в файле
utf8.RuneCountInString(name) 7 — буквы нужна только длина в буквах
[]rune(name) срез рун: каждая буква — отдельный элемент нужно обращаться к буквам по номеру или резать по буквам
len([]rune(name)) 7 — буквы то же, что RuneCountInString, если срез рун всё равно нужен
string(runes[:4]) Болт — первые четыре буквы срез рун обратно в строку

utf8 — пакет unicode/utf8 стандартной библиотеки: в import пишут полный путь "unicode/utf8", а в коде — последнее слово, utf8.

У []rune номера — номера букв: runes[4] — пробел. Разрезать букву пополам так нельзя.


Колонка по буквам

Колонка с первой страницы: название доводим ровно до ширины — короткое дописываем точками, длинное обрезаем и ставим ~. Обе операции — в буквах.

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

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

func fitName(name string, width int) string {
	n := utf8.RuneCountInString(name)
	if n > width {
		runes := []rune(name)
		return string(runes[:width-1]) + "~"
	}
	return name + strings.Repeat(".", width-n)
}

func main() {
	names := []string{"Bolt-M8", "Болт М8", "Шайба", "Анкер клиновой", "Кронштейн усиленный"}
	for _, name := range names {
		fmt.Println(fitName(name, 14) + "|")
	}
}

Что увидите.

Вывод · это печатает программа, набирать не нужно
Bolt-M8.......|
Болт М8.......|
Шайба.........|
Анкер клиновой|
Кронштейн уси~|

Как это читать. В n — число букв. Если букв больше ширины, берём первые width-1 букв через срез рун и дописываем ~: вместе ровно width. Иначе дописываем width-n точек — меньше нуля не бывает: этот случай ушёл в if. В Анкер клиновой ровно четырнадцать букв: ни точек, ни обрезки.

Буквы умеет считать и fmt: ширина %-*s и точность %.*s — в буквах. Такая fitName тоже честная, способ — в course extra 19.


Попробуйте сейчас: буквы вместо байтов.

Цель: длина в байтах и в буквах у одной строки и описание RuneCountInString.

1. Наберите программу и запустите:

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	name := "Болт М8"
	fmt.Println(len(name))
	fmt.Println(utf8.RuneCountInString(name))
	fmt.Println(len([]rune(name)))
	runes := []rune(name)
	fmt.Println(runes)
	fmt.Println(string(runes[:4]))
}
▶ Выполните
cd ~/gocourse/day19/scratch
go run .
go doc utf8.RuneCountInString

Готово, когда: программа напечатала 12, 7, 7, срез чисел и Болт, а go doc — строку func RuneCountInString(s string) (n int) (пункт проверки t_godoc).


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

Цель: колонка в fix1 ровная по буквам на любых названиях; работа записана первым коммитом дня.

1. Запустите заготовку — ширина колонки у вас своя:

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

Русским названиям не хватает точек, а на Анкер клиновой программа падает с negative Repeat count.

2. Почините одну строку: точек — ширина минус число букв. Строка "unicode/utf8" в import в лимит правки не входит. Строки width := и names := не трогайте: проверка подставит в них другие значения.

▶ Выполните
go run .
diff ~/.course/orig/day19/fix1/main.go main.go

3. Заведите репозиторий дня и сделайте первый коммит, как в днях 11–14:

▶ Выполните
cd ~/gocourse/day19
git init
git add .
git commit -m "fix1: колонка по буквам"

Готово, когда: все черты в выводе стоят одна под другой, программа не падает, diff показывает одну изменённую строку, не считая import (пункты fix1 и fixsmall), git log --oneline показывает коммит.


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

Что видите Что это значит Что делать
'Ё' (untyped rune constant 1025) overflows byte байт s[0] сравнили с буквой в одинарных кавычках: 'Ё' — руна, её номер 1025 в байт не помещается начало строки — strings.HasPrefix(s, "Ё"), четвёртая страница
invalid operation: r + "!" (mismatched types rune and untyped string) руну из range склеили со строкой: руна — число, а не строка string(r) + "!"
cannot use runes[:2] (value of type []rune) as string value in variable declaration срез рун положили в строковую переменную обернуть: string(runes[:2])
undefined: utf8 нет import "unicode/utf8" добавить его в import
"unicode/utf8" imported and not used пакет подключён, а ни одной функции из него не вызвано убрать строку из import или вызвать функцию
go vet: conversion from int to string yields a string of one rune, not a string of digits string(qty) с числом даёт не цифры, а знак с таким номером число в строку — strconv.Itoa(qty), как в дне 04
обрезанное название на букву короче или длиннее ширины обрезали width букв и ещё дописали ~ — или наоборот букв width-1, плюс ~

Дальше: как разобрать строку выгрузки на поля и сообщить о битой строке — course next