Viết Reminder Parser dùng Rust
Khi dùng Google Calendar hoặc Reminder.app của macOS, mình rất thích chức năng tạo nhanh một event bằng cách nhập vào nội dung một cách tự nhiên như khi đang nói, ví dụ:
get hair cut at 10am every Sunday
hoặc là
doctor appointment at 1pm on Monday
Khi nhận được input như này, một event mới sẽ được tạo ra với ngày và giờ tương ứng, còn nội dung của event sẽ là phần text mở đầu, ví dụ "get hair cut", hoặc "doctor appointment".
Thường thì cái gì mình thích, mình sẽ tìm cách clone lại, chức năng này cũng ko ngoại lệ.
Vì bài viết này là phần tiếp theo của bài viết trước, chúng ta sẽ tiếp tục dùng Rust và Nom. Recommend các bạn đọc kĩ phần trước, và chuẩn bị những kiến thức cơ bản của Rust, nhất là về Result
, Option
, cargo test
, trước khi đọc tiếp bài này.
Các bạn cũng có thể tham khảo code implementation hoàn chỉnh tại Github trước khi bắt đầu: https://github.com/huytd/reminder-parser
Phân tích cú pháp
Đây không phải là cú pháp mà Google Calendar hay Reminder.app sử dụng, nhưng đây sẽ là cú pháp chúng ta sẽ dùng trong bài viết này, đơn giản là vì nó... đơn giản
Cấu trúc của một event input là một tổ hợp của nhiều token như sau:
" — Bình luận từ đồng nghiệp của tác giả Đây là một thói quen tốt, các bạn cũng nên làm như vậy, tốn time chút nhưng hiệu quả.
#[test]
fn test_parse_time() { let test_times = [ ("at 11:00", Ok(("11", "00", true))), ("at 10pm", Ok(("10", "00", false))), ("at 12:13 am", Ok(("12", "13", true))), ("13:42pm", Ok(("13", "42", false))), ("15:30", Ok(("15", "30", true))), ("at 5", Ok(("5", "00", true))), ("32:412", Ok(("32", "412", true))), ("at 32:281am", Ok(("32", "281", true))), ("at 32pm", Ok(("32", "00", false))), ("night time", Err(())), ("at night", Err(())), ]; for test_case in test_times { let result = parse_time(test_case.0); if test_case.1.is_ok() { let (_, actual) = result.unwrap(); let expected = test_case.1.unwrap(); assert_eq!(actual.hour, expected.0); assert_eq!(actual.minute, expected.1); assert_eq!(actual.meridiem, expected.2); } else { assert!(result.is_err()); } }
}
Trong hàm test_parse_time()
ở trên, ta tạo một mảng test_times
chứa tập các tuple dạng (input, expected_result)
. Trong đó, các input
là các string mà trên thực tế sẽ được nhập vào từ phía user, expected_result
là một giá trị kiểu Result
, nếu nó là giá trị Ok(...)
có nghĩa là input này parse được, còn giá trị Err()
là trường hợp lỗi. Mỗi giá trị Ok(...)
sẽ có dạng (hour, minute, meridiem)
. Trong vòng lặp tiếp theo của hàm test, ta duyệt qua từng test case, lấy ra giá trị thực tế actual
từ parser (kiểu ReminderTime
) và so sánh nó với từng giá trị trong tuple test.
Phương pháp này không có gì mới, bên Golang gọi là table driven testing.
Có test rồi thì ta có thể implement được rồi, quay lại hàm parse_time
, việc đầu tiên là dùng các combinator của nom
để đọc input, nhắc lại cú pháp của <time>
token:
time = ?"at" + hour + ?(":" + minutes) + ?("am"|"pm")
Trước khi đi vào giải thích, đây là hàm parse_time
hoàn chỉnh:
fn parse_time(input: &str) -> IResult<&str, ReminderTime> { let (remain, (_, _, hour, opt_min, _, am)) = tuple(( opt(tag("at")), multispace0, digit1, opt(tuple((tag(":"), digit1))), multispace0, opt(alt((tag("am"), tag("pm")))), )) .parse(input)?; let (_, minute) = opt_min.unwrap_or(("", "00")); let meridiem = am == Some("am") || am == None; Ok((remain, ReminderTime { hour, minute, meridiem }))
}
Implement đầy đủ của hàm parse_date
như sau:
fn parse_date(input: &str) -> IResult<&str, ReminderDate> { let (remain, (_, opt_repeat, _, date)) = tuple(( multispace0, opt(alt((value(true, tag("every")), value(false, tag("on"))))), multispace0, rest )) .parse(input)?; let repeated = opt_repeat.unwrap_or(false); let content = if date.trim().is_empty() { "today" } else { date.trim() }; Ok((remain, ReminderDate { content, repeated }))
}
Như đã viết ở trên, cú pháp của <date>
token là:
date = ?("on"|"every") + date
và hy vọng bài viết này giúp các bạn hiểu rõ hơn về Nom Parser, cũng như kĩ thuật Parser Combination. Lần tới, khi cần parse một nội dung gì đó, thay vì đâm đầu vào sử dụng RegEx, hãy thử chậm lại một tí và sử dụng Nom hay một thư viện Parser Combination nào đó để build, mình nghĩ ngoài đồng nghiệp ra, thì chính bạn trong tương lai sẽ rất cảm kích cái quyết định đó của bản thân. Chúc các bạn may mắn
Hẹn gặp lại các bạn trong các bài viết tiếp theo.