leetcode/tag_validator.rs
1//! # 591. 标签验证器
2//!
3//! 难度 困难
4//!
5//! 给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:
6//!
7//! 1. 代码必须被合法的闭合标签包围。否则,代码是无效的。
8//! 2. 闭合标签(不一定合法)要严格符合格式:`<TAG_NAME>TAG_CONTENT</TAG_NAME>`。其中,`<TAG_NAME>` 是起始标签,`</TAG_NAME>` 是结束标签。起始和结束标签中的 `TAG_NAME` 应当相同。当且仅当 `TAG_NAME` 和 `TAG_CONTENT` 都是合法的,闭合标签才是合法的。
9//! 3. 合法的 `TAG_NAME` 仅含有大写字母,长度在范围 `[1,9]` 之间。否则,该 `TAG_NAME` 是不合法的。
10//! 4. 合法的 `TAG_CONTENT` 可以包含其他合法的闭合标签,`cdata` (请参考规则7)和任意字符(注意参考规则1)除了不匹配的 `<`、不匹配的起始和结束标签、不匹配的或带有不合法 `TAG_NAME` 的闭合标签。否则,`TAG_CONTENT` 是不合法的。
11//! 5. 一个起始标签,如果没有具有相同 `TAG_NAME` 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。
12//! 6. 一个<,如果你找不到一个后续的 `>` 与之匹配,是不合法的。并且当你找到一个 `<` 或 `</` 时,所有直到下一个 `>` 的前的字符,都应当被解析为 `TAG_NAME`(不一定合法)。
13//! 7. `cdata` 有如下格式:`<![CDATA[CDATA_CONTENT]]>` 。`CDATA_CONTENT` 的范围被定义成 `<![CDATA[ 和后续的第一个 ]]>` 之间的字符。
14//! 8. `CDATA_CONTENT` 可以包含任意字符。`cdata` 的功能是阻止验证器解析 `CDATA_CONTENT`,所以即使其中有一些字符可以被解析为标签(无论合法还是不合法),也应该将它们视为常规字符。
15//!
16//! 合法代码的例子:
17//!
18//! ```text
19//! 输入: "<DIV>This is the first line <![CDATA[<div>]]></DIV>"
20//!
21//! 输出: True
22//!
23//! 解释:
24//!
25//! 代码被包含在了闭合的标签内: <DIV> 和 </DIV> 。
26//!
27//! TAG_NAME 是合法的,TAG_CONTENT 包含了一些字符和 cdata 。
28//!
29//! 即使 CDATA_CONTENT 含有不匹配的起始标签和不合法的 TAG_NAME,它应该被视为普通的文本,而不是标签。
30//!
31//! 所以 TAG_CONTENT 是合法的,因此代码是合法的。最终返回True。
32//!
33//!
34//! 输入: "<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"
35//!
36//! 输出: True
37//!
38//! 解释:
39//!
40//! 我们首先将代码分割为: start_tag|tag_content|end_tag 。
41//!
42//! start_tag -> "<DIV>"
43//!
44//! end_tag -> "</DIV>"
45//!
46//! tag_content 也可被分割为: text1|cdata|text2 。
47//!
48//! text1 -> ">> ![cdata[]] "
49//!
50//! cdata -> "<![CDATA[<div>]>]]>" ,其中 CDATA_CONTENT 为 "<div>]>"
51//!
52//! text2 -> "]]>>]"
53//!
54//!
55//! start_tag 不是 "<DIV>>>" 的原因参照规则 6 。
56//! cdata 不是 "<![CDATA[<div>]>]]>]]>" 的原因参照规则 7 。
57//! ```
58//!
59//! 不合法代码的例子:
60//!
61//! ```text
62//! 输入: "<A> <B> </A> </B>"
63//! 输出: False
64//! 解释: 不合法。如果 "<A>" 是闭合的,那么 "<B>" 一定是不匹配的,反之亦然。
65//!
66//! 输入: "<DIV> div tag is not closed <DIV>"
67//! 输出: False
68//!
69//! 输入: "<DIV> unmatched < </DIV>"
70//! 输出: False
71//!
72//! 输入: "<DIV> closed tags with invalid tag name <b>123</b> </DIV>"
73//! 输出: False
74//!
75//! 输入: "<DIV> unmatched tags with invalid tag name </1234567890> and <CDATA[[]]> </DIV>"
76//! 输出: False
77//!
78//! 输入: "<DIV> unmatched start tag <B> and unmatched end tag </C> </DIV>"
79//! 输出: False
80//! ```
81//!
82//! ## 注意:
83//!
84//! 为简明起见,你可以假设输入的代码(包括提到的任意字符)只包含`数字`, `字母`, `'<','>','/','!','[',']'` 和 `' '`。
85//!
86//! See [leetcode](https://leetcode-cn.com/problems/tag-validator/)
87
88/// 用有限状态机,处理几个有限的状态就解决了。
89///
90/// Rust 的好处是很容易写比较高级的抽象。而且**对错误处理非常友好**。语法上主要是 Pattern Matching 和各种高阶函数的 combinator 组合。感觉用 Erlang/Elixir 写起来应该会很方便。
91///
92/// 代码逻辑:
93///
94/// 1. `chars()` 生成字符串的 [`Iterator`](Iterator)
95/// 2. `try_fold` 消费这个 [`Iterator`](Iterator),创建一个新的 `Validator` 对象作为 fold 的初始状态传入。
96/// 3. `try_fold` 的每一次回调处理一个字符,调用 `validator.handle(c)` 返回一个 `Result`。 只要每步都 Ok,那就一直 handle 下去,只要中间发现 Err,就会提早返回 (短路操作)
97/// 4. 最后消费完 [`Iterator`](Iterator) 也就是所有字符都处理完了,需要判断下 `Validator.is_end()` 是否合法结束,即检查 `Validator` 的状态是否是 `End`(完全闭合最外层的标签)。
98///
99/// 主要是 `Validator` 里的几个状态定义:
100///
101/// * `Init` 初始状态,只接收 `<` 就进入 `TagName` 状态,否则失败
102/// * `TagName` 这个状态比较复杂,其实就是 Tag 的定义条件,这里用了一个 `is_close` 来标识是不是 **关闭标签**
103/// * `TagContent` 这个状态简单,只要遇到 `<` 就进入 `TagName` 状态,其它都 Ok
104/// * `CDataTag` 这个也只有一种情况,读取完整的 `[CDATA[` 进入 `CDataContent` 状态,否则失败
105/// * `CDataContent` 这个状态也简单(用了两个 `bool` 标记是否已经有连续两个 `]` ),遇到 `]]>` 就结束,回到 `TagContent` 状态(注意 reset 标记)
106/// * `End` 最外层的 Tag 闭合,结束,不能再有内容(即如果这个状态下再接收到内容,都是 Err)
107///
108/// 这里就 `TagName` 里处理得最多,具体条件结合题目定义,看代码。
109///
110/// 另外,`Validator` 里定义了一个 `stack`,记录 **开标签**,用于与 **闭标签** 匹配。
111///
112/// Rust 的类型系统使得错误处理机制非常严谨,安全。代码里每一个地方都记录了 invalid 的错误信息,可以在最外层调用的地方统一打印。
113///
114/// 因为用 [`Iterator`](Iterator),是 lazy 调用,只有在每一步迭代的时候才会执行代码,只迭代了一遍。(如果要调试输入的情况,在 `try_fold`
115/// 前面加一个 `inspect` 可以打印出每个字符,结合每个地方的状态变化,可以很轻松地调试)。
116/// 因为代码抽象程度比较好,虽然看起来代码长了一点,但结构非常清晰,可读性,安全性,可扩展/维护性都很优秀。
117pub struct Solution;
118
119impl Solution {
120 pub fn is_valid(code: String) -> bool {
121 code.chars().try_fold(Validator::new(), |mut validator, c| {
122 validator.handle(c).map(|_| validator)
123 })
124 .and_then(|validator| match validator.is_end() {
125 true => Ok(()),
126 _ => Err("EOF"),
127 })
128 // .map_err(|e| {
129 // eprintln!("Invalid, err: {}", e);
130 // })
131 .is_ok()
132 }
133}
134
135type Result = std::result::Result<(), &'static str>;
136
137enum State {
138 Init,
139 TagName {
140 cache: String,
141 is_close: bool,
142 },
143 TagContent,
144 CDataTag {
145 cache: String,
146 },
147 CDataContent {
148 // stand for close prefix `]]`
149 prefix: (bool, bool),
150 },
151 End,
152}
153
154struct Validator {
155 state: State,
156 stack: Vec<String>,
157}
158
159impl Validator {
160 fn new() -> Self {
161 Self {
162 state: State::Init,
163 stack: Vec::new(),
164 }
165 }
166}
167
168impl Validator {
169 fn is_end(&self) -> bool {
170 match &self.state {
171 State::End => true,
172 _ => false,
173 }
174 }
175
176 fn handle(&mut self, c: char) -> Result {
177 match &self.state {
178 State::Init => self.handle_init(c),
179 State::TagName { .. } => self.handle_tag_name(c),
180 State::TagContent => self.handle_tag_content(c),
181 State::CDataTag { .. } => self.handle_cdata_tag(c),
182 State::CDataContent { .. } => self.handle_cdata_content(c),
183 State::End => self.handle_end(c),
184 }
185 }
186
187 // In the Init state, only accept '<' character then go into next state: TagName
188 fn handle_init(&mut self, c: char) -> Result {
189 match (&self.state, c) {
190 (State::Init, '<') => {
191 self.state = State::TagName {
192 cache: String::new(),
193 is_close: false,
194 };
195 Ok(())
196 }
197 (State::Init, _) => Err("Expect <"),
198 _ => panic!("Invalid State, expect Init"),
199 }
200 }
201
202 fn handle_tag_name(&mut self, c: char) -> Result {
203 match self.state {
204 State::TagName { ref mut cache, ref mut is_close } => {
205 match (c, cache.len(), &is_close) {
206 // meet 'A'..='Z', tag name cache should less then 9
207 (c @ 'A' ..= 'Z', 0..=8, _) => {
208 cache.push(c);
209 Ok(())
210 }
211 // meet '>', cache length should in 1..=9
212 // if not is_close then into TagContent
213 ('>', 1..=9, false) => {
214 self.stack.push(cache.to_string());
215 self.state = State::TagContent;
216 Ok(())
217 }
218 // if is_close then into TagContent or End
219 ('>', 1..=9, true) => {
220 match self.stack.pop() {
221 Some(s) if s == *cache => {
222 if self.stack.len() > 0 {
223 // inner level tag closed, into outer TagContent
224 self.state = State::TagContent;
225 } else {
226 // top level tag closed, into End
227 self.state = State::End;
228 }
229 Ok(())
230 }
231 _ => Err("Wrong close tag"),
232 }
233 }
234 // meet '/' only when not is_close and cache is empty
235 ('/', 0, false) => {
236 *is_close = true;
237 Ok(())
238 }
239 // otherwise invalid tag character
240 // ('/', _, true) | ('/', _, false) => Err("Invalid tag char"),
241 // meet '!' only when cache is empty & not !is_close, which means '!' just after '<'
242 ('!', 0, false) if !self.stack.is_empty() => {
243 self.state = State::CDataTag { cache: String::new() };
244 Ok(())
245 }
246 // you can match more different conditions for more error detail
247 _ => Err("Invalid character in TagName State"),
248 }
249 }
250 _ => panic!("Invalid State, expect TagName"),
251 }
252 }
253
254 fn handle_tag_content(&mut self, c: char) -> Result {
255 match (&self.state, c) {
256 // if meet '<', then into inner TagName
257 (State::TagContent, '<') => {
258 self.state = State::TagName {
259 cache: String::new(),
260 is_close: false,
261 };
262 Ok(())
263 }
264 // otherwise all characters would be ok
265 (State::TagContent, _) => Ok(()),
266 _ => panic!("Invalid State, expect TagContent"),
267 }
268 }
269
270 fn handle_cdata_tag(&mut self, c: char) -> Result {
271 match self.state {
272 State::CDataTag { ref mut cache } => {
273 match (c, cache.len()) {
274 (c, 0..=6) => {
275 cache.push(c);
276 if "[CDATA[" == cache.as_str() {
277 self.state = State::CDataContent { prefix: (false, false) };
278 Ok(())
279 } else if "[CDATA[".starts_with(cache.as_str()) {
280 Ok(())
281 } else {
282 Err("Wrong CDataTag character")
283 }
284 }
285 _ => Err("CDataTag invalid"),
286 }
287 }
288 _ => panic!("Invalid State, expect CDataTag"),
289 }
290 }
291
292 fn handle_cdata_content(&mut self, c: char) -> Result {
293 match self.state {
294 State::CDataContent { ref mut prefix } => {
295 match (&prefix.0, &prefix.1, c) {
296 (true, true, '>') => {
297 self.state = State::TagContent;
298 Ok(())
299 }
300 (true, true, ']') => Ok(()),
301 (true, true, _) => {
302 // reset prefix
303 *prefix = (false, false);
304 Ok(())
305 }
306 (true, false, ']') => {
307 *prefix = (true, true);
308 Ok(())
309 }
310 (false, false, ']') => {
311 *prefix = (true, false);
312 Ok(())
313 }
314 _ => Ok(()),
315 }
316 }
317 _ => panic!("Invalid State, expect CDataContent"),
318 }
319 }
320
321 fn handle_end(&mut self, _c: char) -> Result {
322 match self.state {
323 State::End => Err("End of code"),
324 _ => panic!("Invalid State, expect End"),
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test() {
335 let cases = vec![
336 (true, "<DIV>This is the first line <![CDATA[<div>]]></DIV>"),
337 (true, "<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"),
338 (true, "<TRUE><![CDATA[wahaha]]]><![CDATA[]> wahaha]]></TRUE>"),
339
340 (false, "<DIV>>> ![cdata[]] </![CDATA[<div>]>]]>]]>>]</DIV>"),
341 (false, "<A> <B> </A> </B>"),
342 (false, "<DIV> div tag is not closed <DIV>"),
343 (false, "<DIV> unmatched < </DIV>"),
344 (false, "<DIV> closed tags with invalid tag name <b>123</b> </DIV>"),
345 (false, "<DIV> unmatched tags with invalid tag name </1234567890> and <CDATA[[]]> </DIV>"),
346 (false, "<DIV> unmatched start tag <B> and unmatched end tag </C> </DIV>"),
347 (false, "<A><![CDATA[</A>]]123></A>"),
348 ];
349 for (expect, arg) in cases {
350 assert_eq!(expect, Solution::is_valid(arg.into()));
351 }
352 }
353}