#
Основы работы с Unicode в Zig
#
Символы
Понятие символа ("character") довольно запутанное. Пойдем по порядку.
В Zig литеральный символ русского алфавита ('e'), вопреки ожиданиям, основанным на опыте работы с другими языками программирования, имеет тип comptime_int и может быть принудительно приведен к типу u8 для обработки чисто ASCII-текстов или к типу u21 для работы с любыми Unicode-кодами.
fn processAscii(byte: u8) void { _ = byte; }
fn processUnicode(code_point: u21) void { _ = code_point; }
// Вывод: comptime_int
debug.print("{}\n", .{@TypeOf('е')}); // русская буква 'е'
processAscii('е'); // приведение к u8 невозможно, так как 'е' выходит за рамки ASCII
processUnicode('е'); // корректно
Заметим, что в Zig литералы символов ('e') автоматически становятся значениями типа comptime_int, и их можно принудительно приводить к типам u8 для работы с ASCII или автоматически к типу u21 для работы с полными Unicode-кодами.
#
ASCII в Zig
Для работы с односимвольными (одно-байтовыми) ASCII-символами Zig предоставляет пространство имен std.ascii, где собраны полезные функции.
const ascii = @import("std").ascii;
_ = ascii.isAlphanumeric('А'); // кириллица, не сработает, поскольку проверяется как ASCII
_ = ascii.isAlphabetic('А'); // аналогично
_ = ascii.isDigit('3'); // цифра, работает корректно
#
Кодовые точки Unicode
Каждой букве, знаку пунктуации, эмодзи и прочему в Unicode присвоен уникальный номер — кодовая точка (code point). Эти номера кодируются в кодовые единицы (code units) с помощью формы кодирования Unicode. Сегодня самая распространенная форма — это UTF-8, где каждая кодовая точка может быть представлена одной или несколькими кодовыми единицами по 8 бит каждая.
UTF-8 спроектирован таким образом, что первые кодовые точки (ASCII-символы) могут быть закодированы всего одним байтом, причем этот байт совпадает с ASCII-значением соответствующего символа. То есть UTF-8-текст, содержащий только ASCII-символы, практически неотличим от чистого ASCII-текста, что позволяет использовать старый код, рассчитанный на ASCII.
#
Кодовые точки в Zig
Весь исходный код на Zig представлен в виде UTF-8-текста, и пространство имен std.unicode предоставляет некоторые функции для работы с кодовыми точками.
const unicode = @import("std").unicode;
// UTF-8 кодовая точка может быть длиной от 1 до 4 байт.
var code_point_bytes: [4]u8 = undefined;
// Возвращает количество записанных байт.
const bytes_encoded = try unicode.utf8Encode('✨', &code_point_bytes);
Кроме того, имеется функция utf8Decode и функции для конвертации между UTF-8 и UTF-16, а также утилиты для итерации по кодовым точкам строки.
#
Строки в Zig
Как объясняют коллеги, строковые литералы в Zig — это просто указатели на нулевые байты массивов, спрятанные в исполняемый файл программы. Когда вы пишете:
const name = "Василий";
это синтаксический сахар, который за кулисами создает массив и указатель на него, но в итоге это просто последовательность байтов. Так как эти байты фактически закодированы в UTF-8, мы можем использовать функции для работы с Unicode, чтобы обрабатывать строки в Zig. Например, итерируем по кодовым точкам строки:
const unicode = @import("std").unicode;
const name = "Василий";
var code_point_iterator = (try unicode.Utf8View.init(name)).iterator();
while (code_point_iterator.nextCodepoint()) |code_point| {
std.debug.print("0x{x} is {u} \n", .{ code_point, code_point });
}
#
Кодовые точки — это не всегда символы
Одна из проблем, которая может возникнуть при итерации строк по одной кодовой точке за раз, — это случайное разделение символа, состоящего из нескольких кодовых точек. Существует заблуждение, вероятно восходящее к эпохе ASCII, будто кодовая точка и символ совпадают, и это утверждение зачастую справедливо, но не всегда.
const unicode = @import("std").unicode;
fn codePointInfo(str: []const u8) !void {
std.debug.print("Кодовые точки для: {s} \n", .{str});
var iter = (try unicode.Utf8View.init(str)).iterator();
while (iter.nextCodepoint()) |cp| {
std.debug.print("0x{x} is {u} \n", .{ cp, cp });
}
}
try codePointInfo("Й");
try codePointInfo("\u{418}\u{306}");
Обратите внимание на вывод: обе строки отображаются одинаково как один символ "Й", но первая строка содержит одну кодовую точку, а вторая — две. Вторая версия, состоящая из двух кодовых точек, представляет собой базовый символ 0x418 (буква 'И') и комбинируемый знак 0x306 (крышечку). Подобным образом существует множество многокомпонентных символов, таких как корейские буквы, флаги стран и модифицированные эмодзи, которые нельзя корректно обработать как одиночную кодовую точку. Unicode предоставляет алгоритмы для обработки последовательностей кодовых точек и формирования высших абстракций, таких как графемы, слова и предложения.
#
Байты все равно полезны!
Интересно отметить, что на нижнем уровне мы все еще имеем дело с последовательностями байтов. Если все, что нам нужно, — это сопоставить байты один к одному, то не нужно прибегать к высоким концепциям. Пространство имен std.mem предоставляет множество полезных функций, работающих с любым видом последовательностей, включая байты UTF-8-строк.
const mem = @import("std").mem;
_ = mem.eql(u8, "✨ Zig!", "✨ Zig!");
_ = mem.trimLeft(u8, "✨ Zig!", "✨");
_ = mem.trimRight(u8, "✨ Zig!", "!");
_ = mem.trim(u8, " ✨ Zig! ", " ");
_ = mem.indexOf(u8, "✨ Zig!", "Z");
_ = mem.split(u8, "✨ Zig!", " ");
#
Это все, что можно сделать?
Из этого поста мы выносим два основных урока:
- Байт может быть символом, но многие символы требуют больше одного байта.
- Кодовая точка может быть символом, но многие символы требуют больше одной кодовой точки.
Мы увидели некоторые полезные инструменты, предоставляемые Zig для обработки UTF-8 Unicode-текстов. Помимо этих инструментов, существует множество других высокоуровневых абстракций, которые можно использовать с помощью сторонних библиотек.
Оригинал статьи на английском языке здесь