cleanup
[pspp] / rust / src / identifier.rs
1 use std::fmt::{Display, Formatter, Result as FmtResult};
2
3 use encoding_rs::{EncoderResult, Encoding};
4 use finl_unicode::categories::{CharacterCategories, MajorCategory};
5 use thiserror::Error as ThisError;
6 use unicase::UniCase;
7
8 pub trait IdentifierChar {
9     /// Returns true if `self` may be the first character in an identifier.
10     fn may_start_id(self) -> bool;
11
12     /// Returns true if `self` may be a second or subsequent character in an
13     /// identifier.
14     fn may_continue_id(self) -> bool;
15 }
16
17 impl IdentifierChar for char {
18     fn may_start_id(self) -> bool {
19         use MajorCategory::*;
20
21         ([L, M, S].contains(&self.get_major_category()) || "@#$".contains(self))
22             && self != char::REPLACEMENT_CHARACTER
23     }
24
25     fn may_continue_id(self) -> bool {
26         use MajorCategory::*;
27
28         ([L, M, S, N].contains(&self.get_major_category()) || "@#$._".contains(self))
29             && self != char::REPLACEMENT_CHARACTER
30     }
31 }
32
33 #[derive(Clone, Debug, ThisError)]
34 pub enum Error {
35     #[error("Identifier cannot be empty string.")]
36     Empty,
37
38     #[error("\"{0}\" may not be used as an identifier because it is a reserved word.")]
39     Reserved(String),
40
41     #[error("\"{0}\" may not be used as an identifier because it begins with disallowed character \"{1}\".")]
42     BadFirstCharacter(String, char),
43
44     #[error("\"{0}\" may not be used as an identifier because it contains disallowed character \"{1}\".")]
45     BadLaterCharacter(String, char),
46
47     #[error("Identifier \"{id}\" is {length} bytes in the encoding in use ({encoding}), which exceeds the {max}-byte limit.")]
48     TooLong {
49         id: String,
50         length: usize,
51         encoding: &'static str,
52         max: usize,
53     },
54
55     #[error("\"{id}\" may not be used as an identifier because the encoding in use ({encoding}) cannot represent \"{c}\".")]
56     NotEncodable {
57         id: String,
58         encoding: &'static str,
59         c: char,
60     },
61 }
62
63 fn is_reserved_word(s: &str) -> bool {
64     for word in [
65         "and", "or", "not", "eq", "ge", "gt", "le", "ne", "all", "by", "to", "with",
66     ] {
67         if s.eq_ignore_ascii_case(word) {
68             return true;
69         }
70     }
71     false
72 }
73
74 #[derive(Clone, PartialEq, Eq, Debug, Hash)]
75 pub struct Identifier(pub UniCase<String>);
76
77 impl Identifier {
78     /// Maximum length of an identifier, in bytes.  The limit applies in the
79     /// encoding used by the dictionary, not in UTF-8.
80     pub const MAX_LEN: usize = 64;
81
82     pub fn new(s: &str, encoding: &'static Encoding) -> Result<Identifier, Error> {
83         Self::is_plausible(s)?;
84         let (encoded, _, unencodable) = encoding.encode(s);
85         if unencodable {
86             let mut encoder = encoding.new_encoder();
87             let mut buf = Vec::with_capacity(
88                 encoder
89                     .max_buffer_length_from_utf8_without_replacement(s.len())
90                     .unwrap(),
91             );
92             let EncoderResult::Unmappable(c) = encoder
93                 .encode_from_utf8_to_vec_without_replacement(s, &mut buf, true)
94                 .0
95             else {
96                 unreachable!();
97             };
98             return Err(Error::NotEncodable {
99                 id: s.into(),
100                 encoding: encoding.name(),
101                 c,
102             });
103         }
104         if encoded.len() > Self::MAX_LEN {
105             return Err(Error::TooLong {
106                 id: s.into(),
107                 length: encoded.len(),
108                 encoding: encoding.name(),
109                 max: Self::MAX_LEN,
110             });
111         }
112         Ok(Identifier(s.into()))
113     }
114     pub fn is_plausible(s: &str) -> Result<(), Error> {
115         if s.is_empty() {
116             return Err(Error::Empty);
117         }
118         if is_reserved_word(s) {
119             return Err(Error::Reserved(s.into()));
120         }
121
122         let mut i = s.chars();
123         let first = i.next().unwrap();
124         if !first.may_start_id() {
125             return Err(Error::BadFirstCharacter(s.into(), first));
126         }
127         for c in i {
128             if !c.may_continue_id() {
129                 return Err(Error::BadLaterCharacter(s.into(), c));
130             }
131         }
132         Ok(())
133     }
134 }
135
136 impl Display for Identifier {
137     fn fmt(&self, f: &mut Formatter) -> FmtResult {
138         write!(f, "{}", self.0)
139     }
140 }