1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct ScriptCode {
9 value: String,
10}
11
12impl ScriptCode {
13 #[must_use]
15 pub fn new(input: &str) -> Option<Self> {
16 parse_script_code(input)
17 }
18
19 #[must_use]
21 pub fn as_str(&self) -> &str {
22 &self.value
23 }
24
25 #[must_use]
27 pub fn into_string(self) -> String {
28 self.value
29 }
30}
31
32impl AsRef<str> for ScriptCode {
33 fn as_ref(&self) -> &str {
34 self.as_str()
35 }
36}
37
38impl fmt::Display for ScriptCode {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(self.as_str())
41 }
42}
43
44#[must_use]
46pub fn parse_script_code(input: &str) -> Option<ScriptCode> {
47 normalize_script_code(input).map(|value| ScriptCode { value })
48}
49
50#[must_use]
52pub fn is_script_code(input: &str) -> bool {
53 normalize_script_code(input).is_some()
54}
55
56#[must_use]
58pub fn normalize_script_code(input: &str) -> Option<String> {
59 let trimmed = input.trim();
60 if trimmed.len() != 4 || !trimmed.bytes().all(|byte| byte.is_ascii_alphabetic()) {
61 return None;
62 }
63
64 let mut normalized = String::with_capacity(4);
65 for (index, character) in trimmed.chars().enumerate() {
66 if index == 0 {
67 normalized.push(character.to_ascii_uppercase());
68 } else {
69 normalized.push(character.to_ascii_lowercase());
70 }
71 }
72
73 Some(normalized)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::{ScriptCode, is_script_code, normalize_script_code, parse_script_code};
79
80 #[test]
81 fn accepts_common_script_examples() {
82 for script in ["Latn", "Cyrl", "Arab", "Hans", "Hant"] {
83 assert!(is_script_code(script));
84 assert_eq!(parse_script_code(script).unwrap().as_str(), script);
85 }
86 }
87
88 #[test]
89 fn normalizes_to_title_case() {
90 assert_eq!(normalize_script_code("latn"), Some("Latn".to_string()));
91 assert_eq!(normalize_script_code("CYRL"), Some("Cyrl".to_string()));
92 assert_eq!(ScriptCode::new(" hAnT ").unwrap().as_str(), "Hant");
93 }
94
95 #[test]
96 fn rejects_invalid_script_shapes() {
97 for script in ["", "Lat", "Latnn", "La1n", "Latn-US", "漢字"] {
98 assert!(!is_script_code(script));
99 assert!(parse_script_code(script).is_none());
100 }
101 }
102}