drawdag/
succ.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8/// String successor by incrementing characters.
9/// Similar to Ruby's `String#succ` [1], except if all characters are
10/// non-alphanumeric, this function appends `1` to the end.
11///
12/// [1]: https://ruby-doc.org/core-3.1.0/String.html#method-i-next
13pub(crate) fn str_succ(s: &str) -> String {
14    let mut chars: Vec<char> = s.chars().collect();
15
16    // The first character to be incremented is the rightmost alphanumeric.
17    let index: Option<usize> = chars
18        .iter()
19        .enumerate()
20        .rev()
21        .filter_map(|(i, c)| CharRange::from_char(*c).map(|_| i))
22        .next();
23    match index {
24        Some(index) => {
25            let mut carry: char = '1';
26            for i in (0..=index).rev() {
27                let ch = chars[i];
28                let range = match CharRange::from_char(ch) {
29                    None => {
30                        chars.insert(i + 1, carry);
31                        break;
32                    }
33                    Some(range) => range,
34                };
35                carry = range.carry();
36                let (start, end) = range.bound();
37                if ch == end {
38                    chars[i] = start;
39                    if i == 0 {
40                        chars.insert(0, range.carry());
41                    }
42                } else {
43                    chars[i] = ((ch as u8) + 1) as char;
44                    break;
45                }
46            }
47            chars.into_iter().collect()
48        }
49        None => format!("{}1", s),
50    }
51}
52
53/// Range of an ASCII char.
54#[derive(Copy, Clone, PartialEq)]
55enum CharRange {
56    Digit,
57    LowerLetter,
58    UpperLetter,
59}
60
61impl CharRange {
62    const ALL: &'static [Self] = &[Self::Digit, Self::LowerLetter, Self::UpperLetter];
63
64    /// Get the start and end char in the range.
65    fn bound(self) -> (char, char) {
66        match self {
67            CharRange::Digit => ('0', '9'),
68            CharRange::LowerLetter => ('a', 'z'),
69            CharRange::UpperLetter => ('A', 'Z'),
70        }
71    }
72
73    /// Get the char used when carrying to a new char.
74    fn carry(self) -> char {
75        match self {
76            CharRange::Digit => '1',
77            CharRange::LowerLetter => 'a',
78            CharRange::UpperLetter => 'A',
79        }
80    }
81
82    /// Convert a char to `CharRange` if it's in a known range.
83    fn from_char(ch: char) -> Option<CharRange> {
84        Self::ALL.iter().copied().find(|range| {
85            let (start, end) = range.bound();
86            ch >= start && ch <= end
87        })
88    }
89}
90
91#[cfg(test)]
92#[test]
93fn test_str_succ() {
94    // Alphanumeric cases are consistent with Ruby.
95    assert_eq!(str_succ("0"), "1");
96    assert_eq!(str_succ("1"), "2");
97    assert_eq!(str_succ("9"), "10");
98    assert_eq!(str_succ("233"), "234");
99    assert_eq!(str_succ("999"), "1000");
100    assert_eq!(str_succ("0099"), "0100");
101    assert_eq!(str_succ("1180591620717411303424"), "1180591620717411303425");
102
103    assert_eq!(str_succ("a-99"), "a-100");
104    assert_eq!(str_succ("aa99"), "ab00");
105    assert_eq!(str_succ("aa99.."), "ab00..");
106
107    assert_eq!(str_succ("a"), "b");
108    assert_eq!(str_succ("C"), "D");
109    assert_eq!(str_succ("ASDF"), "ASDG");
110    assert_eq!(str_succ("zz"), "aaa");
111
112    assert_eq!(str_succ("Zz"), "AAa");
113    assert_eq!(str_succ("9z9Z"), "10a0A");
114
115    // Non-alphanumeric cases are different from Ruby.
116    assert_eq!(str_succ(""), "1");
117    assert_eq!(str_succ("."), ".1");
118}