Zig std.mem.tokenizeAny: แยกคำเมื่อ delimiter มีหลายตัว
ข้อมูลจาก command line และไฟล์ config เก่ามักไม่ได้ใช้ตัวคั่นชนิดเดียว บางช่องเป็น comma บางช่องเป็น slash และมี space แทรก ถ้าเขียน split หลายรอบ เราจะได้ iterator ซ้อนกันและเผลอเก็บ token ว่าง std.mem.tokenizeAny รับชุด delimiter แล้วถือว่า ตัวใดตัวหนึ่ง ในชุดนั้นเป็นขอบคำครับ
WHY: token กับ split ตอบคนละคำถาม
tokenizeAny(u8, input, " ,/|") เหมือนอิกัวนาคัดของบนสายพาน เจอลูกปัดตัวคั่นทรงไหนก็เริ่มช่องใหม่ และข้ามลูกปัดที่ติดกัน จึงไม่คืน token ว่าง เหมาะกับ tag, search path แบบ custom หรือรายการชื่อที่ whitespace ซ้ำไม่มีความหมาย
ถ้าช่องว่างมีความหมาย เช่น CSV ที่ a,,b แปลว่าคอลัมน์กลางว่าง tokenizeAny เป็นเครื่องมือผิด เพราะมันจะคืนแค่ a กับ b กรณีนั้นใช้ splitAny หรือ parser ของ format นั้นโดยตรง โดยเฉพาะ CSV ที่มี quote และ escape ไม่ควรทำเองด้วย delimiter iterator
HOW: แยกหลาย delimiter และยืนยันว่าไม่เก็บช่องว่าง
const std = @import("std");
test "tokenizeAny treats every delimiter byte as a boundary" {
const input = "alpha, beta//gamma|delta epsilon";
var it = std.mem.tokenizeAny(u8, input, " ,/|");
const expected = [_][]const u8{
"alpha", "beta", "gamma", "delta", "epsilon",
};
var index: usize = 0;
while (it.next()) |token| : (index += 1) {
try std.testing.expect(index < expected.len);
try std.testing.expectEqualStrings(expected[index], token);
}
try std.testing.expectEqual(expected.len, index);
}
test "empty input and only delimiters produce no tokens" {
var empty = std.mem.tokenizeAny(u8, "", " ,");
try std.testing.expect(empty.next() == null);
var separators = std.mem.tokenizeAny(u8, " ,,, ", " ,");
try std.testing.expect(separators.next() == null);
}
ฟังก์ชันไม่ allocate ตัว token; สิ่งที่ next() คืนคือ slice ที่อ้าง input เดิม เวลาเดินรวมเป็น O(n) ส่วนการตรวจ delimiter ต่อ byte ขึ้นกับชนิดและจำนวน delimiter ใน implementation ดังนั้นชุดเล็กคงที่เหมาะที่สุด ที่สำคัญ input ต้องมีอายุยาวกว่า token ที่เราเก็บไว้ compiler จะช่วยบังคับ lifetime ผ่าน slice
เลือก tokenize ตัวไหน
tokenizeScalar ใช้เมื่อ delimiter มีตัวเดียว เช่น space หนึ่ง byte และอยากข้ามช่องว่างซ้ำ tokenizeSequence ใช้เมื่อ boundary เป็นลำดับติดกันทั้งชุด เช่น "::" ส่วน tokenizeAny ใช้เมื่อ byte ใดก็ได้ ในชุดเป็น boundary เช่น space, comma หรือ slash
ชื่อ Any ไม่ได้แปลว่ารองรับ Unicode grapheme โดยอัตโนมัติ ตัวอย่างใช้ u8 จึงทำงานระดับ byte สำหรับ ASCII delimiters ถ้าต้องแยกตามอักขระ Unicode ต้องตัดสินใจเรื่อง encoding และ normalization ให้ชัด การส่ง byte ของตัวอักษรหลาย byte เป็น delimiter set อาจตัดกลาง UTF-8 และสร้าง token ที่ไม่ถูกต้อง
อีกขอบสำคัญคือ iterator อ้าง buffer เดิม อย่าแก้ buffer ระหว่างเดิน และถ้าจะเก็บ token หลัง buffer หาย ต้อง copy ลง allocator ที่มี owner ชัดเจน อย่าแปลงทุก token เป็น allocationโดยอัตโนมัติถ้าใช้แค่ใน loop เพราะจะเสียข้อดีหลักของ API
Recap
- ใช้
tokenizeAnyเมื่อ delimiter ตัวใดตัวหนึ่งในชุดกั้นคำ - จำว่า delimiter ติดกันไม่สร้าง token ว่าง
- ใช้
splitAnyหรือ parser เฉพาะเมื่อช่องว่างมีความหมาย - token ยืม input เดิม ถ้าจะเก็บระยะยาวต้อง copy อย่างมี owner
- ระวัง Unicode เมื่อทำงานระดับ
u8
เริ่มจาก test ที่มี delimiter ซ้ำ ติดกัน และ input ว่างครับ เพราะสามกรณีนี้บอกได้เร็วที่สุดว่าเราเลือกตระกูล tokenize หรือ split ถูกหรือไม่