cliff3_util/
string_util.rs

1//! 문자열 관련 유틸리티 함수 모음
2//!
3//! 한글 초/중/종성 분리 관련 소스 출처는 [가사시니](https://gs.saro.me/2018/10/01/백업-가리사니-자바-한글분해-Stream-API,-StringBuilder,-raw-속도-테스트.html)님 블로그 입니다.
4
5use crate::error::MissingArgumentError;
6use lazy_static::lazy_static;
7use rand::Rng;
8use regex::Regex;
9
10// 마스킹 처리용 문자
11// const APPLY_MASK: &str = "*";
12
13lazy_static! {
14    /// 이메일 정규식
15    static ref EMAIL_REGEX: Regex = Regex::new(r"^[\w\-]+(\.[\w\-]+)*@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$").unwrap();
16
17    static ref RANDOM_SOURCE: Vec<&'static str> = vec![
18        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g",
19        "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y",
20        "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
21        "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
22    ];
23
24    static ref RANDOM_SOURCE_SPEC: Vec<&'static str> = vec![
25        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g",
26        "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y",
27        "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
28        "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "~", "`", "!", "@", "#", "$", "%", "^", "&",
29        "*", "(", ")", "-", "_", "=", "+", "[", "{", "]", "}", ";", ":", "'", "\"", ",", "<", ".",
30        ">", "/", "?", "\\"
31    ];
32
33    // -----------------------------------------------------------------------------------------------------------------
34    // 한글 관련
35    // -----------------------------------------------------------------------------------------------------------------
36    /// 한글 자음(초성)
37    static ref KO_CONSONANTS: Vec<char> = vec![
38        'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ',
39        'ㅌ', 'ㅍ', 'ㅎ',
40    ];
41
42    /// 한글 자음 분해(된소리 포함)
43    static ref KO_SEPARATED_CONSONANTS: Vec<Vec<char>> = vec![
44        vec!['ㄱ'],
45        vec!['ㄱ', 'ㄱ'],
46        vec!['ㄴ'],
47        vec!['ㄷ'],
48        vec!['ㄷ', 'ㄷ'],
49        vec!['ㄹ'],
50        vec!['ㅁ'],
51        vec!['ㅂ'],
52        vec!['ㅂ', 'ㅂ'],
53        vec!['ㅅ'],
54        vec!['ㅅ', 'ㅅ'],
55        vec!['ㅇ'],
56        vec!['ㅈ'],
57        vec!['ㅈ', 'ㅈ'],
58        vec!['ㅊ'],
59        vec!['ㅋ'],
60        vec!['ㅌ'],
61        vec!['ㅍ'],
62        vec!['ㅎ'],
63    ];
64
65    /// 한글 모음
66    static ref KO_VOWELS: Vec<char> = vec![
67        'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ',
68        'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ',
69    ];
70
71    /// 한글 모음 분해
72    static ref KO_SEPARATED_VOWELS: Vec<Vec<char>> = vec![
73        vec!['ㅏ'],
74        vec!['ㅐ'],
75        vec!['ㅑ'],
76        vec!['ㅒ'],
77        vec!['ㅓ'],
78        vec!['ㅔ'],
79        vec!['ㅕ'],
80        vec!['ㅖ'],
81        vec!['ㅗ'],
82        vec!['ㅗ', 'ㅏ'],
83        vec!['ㅗ', 'ㅐ'],
84        vec!['ㅗ', 'ㅣ'],
85        vec!['ㅛ'],
86        vec!['ㅜ'],
87        vec!['ㅜ', 'ㅓ'],
88        vec!['ㅜ', 'ㅔ'],
89        vec!['ㅜ', 'ㅣ'],
90        vec!['ㅠ'],
91        vec!['ㅡ'],
92        vec!['ㅡ', 'ㅣ'],
93        vec!['ㅣ'],
94    ];
95
96    /// 한글 받침
97    static ref KO_FINAL_CONSONANTS: Vec<char> = vec![
98        0 as char, 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ',
99        'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ',
100    ];
101
102    /// 한글 받침 분해
103    static ref KO_SEPARATED_FINAL_CONSONANTS: Vec<Vec<char>> = vec![
104        vec![],
105        vec!['ㄱ'],
106        vec!['ㄱ', 'ㄱ'],
107        vec!['ㄱ', 'ㅅ'],
108        vec!['ㄴ'],
109        vec!['ㄴ', 'ㅈ'],
110        vec!['ㄴ', 'ㅎ'],
111        vec!['ㄷ'],
112        vec!['ㄹ'],
113        vec!['ㄹ', 'ㄱ'],
114        vec!['ㄹ', 'ㅁ'],
115        vec!['ㄹ', 'ㅂ'],
116        vec!['ㄹ', 'ㅅ'],
117        vec!['ㄹ', 'ㅌ'],
118        vec!['ㄹ', 'ㅍ'],
119        vec!['ㄹ', 'ㅎ'],
120        vec!['ㅁ'],
121        vec!['ㅂ'],
122        vec!['ㅂ', 'ㅅ'],
123        vec!['ㅅ'],
124        vec!['ㅅ', 'ㅅ'],
125        vec!['ㅇ'],
126        vec!['ㅈ'],
127        vec!['ㅊ'],
128        vec!['ㅋ'],
129        vec!['ㅌ'],
130        vec!['ㅍ'],
131        vec!['ㅎ'],
132    ];
133
134    /// 한글 쌍자음/이중 모음 분해
135    static ref KO_SEPARATED_FORTES_VOWELS: Vec<Vec<char>> = vec![
136        vec!['ㄱ'],
137        vec!['ㄱ', 'ㄱ'],
138        vec!['ㄱ', 'ㅅ'],
139        vec!['ㄴ'],
140        vec!['ㄴ', 'ㅈ'],
141        vec!['ㄴ', 'ㅎ'],
142        vec!['ㄷ'],
143        vec!['ㄸ'],
144        vec!['ㄹ'],
145        vec!['ㄹ', 'ㄱ'],
146        vec!['ㄹ', 'ㅁ'],
147        vec!['ㄹ', 'ㅂ'],
148        vec!['ㄹ', 'ㅅ'],
149        vec!['ㄹ', 'ㄷ'],
150        vec!['ㄹ', 'ㅍ'],
151        vec!['ㄹ', 'ㅎ'],
152        vec!['ㅁ'],
153        vec!['ㅂ'],
154        vec!['ㅂ', 'ㅂ'],
155        vec!['ㅂ', 'ㅅ'],
156        vec!['ㅅ'],
157        vec!['ㅅ', 'ㅅ'],
158        vec!['ㅇ'],
159        vec!['ㅈ'],
160        vec!['ㅈ', 'ㅈ'],
161        vec!['ㅊ'],
162        vec!['ㅋ'],
163        vec!['ㅌ'],
164        vec!['ㅍ'],
165        vec!['ㅎ'],
166        vec!['ㅏ'],
167        vec!['ㅐ'],
168        vec!['ㅑ'],
169        vec!['ㅒ'],
170        vec!['ㅓ'],
171        vec!['ㅔ'],
172        vec!['ㅕ'],
173        vec!['ㅖ'],
174        vec!['ㅗ'],
175        vec!['ㅗ', 'ㅏ'],
176        vec!['ㅗ', 'ㅐ'],
177        vec!['ㅗ', 'ㅣ'],
178        vec!['ㅛ'],
179        vec!['ㅜ'],
180        vec!['ㅜ', 'ㅓ'],
181        vec!['ㅜ', 'ㅔ'],
182        vec!['ㅜ', 'ㅣ'],
183        vec!['ㅠ'],
184        vec!['ㅡ'],
185        vec!['ㅡ', 'ㅣ'],
186        vec!['ㅣ'],
187    ];
188}
189
190/// 주어진 이메일 주소의 유효성 검사 결과를 반환한다.
191///
192/// 만약 대상 문자열이 `None`일 경우 [`MissingArgumentError`]를 반환한다.
193pub fn validate_email(target: Option<&str>) -> Result<bool, MissingArgumentError> {
194    // TODO(joonho): 2023-10-03 한글 도메인 및 ID 포함
195    match target {
196        None => Err(MissingArgumentError::default()),
197        Some(v) => Ok(EMAIL_REGEX.is_match(v)),
198    }
199}
200
201/// 주어진 문자열에서 한글 초성만 추출.
202///
203/// 한글이 아닌 다른 문자(한자, 알파벳, 이모티콘, 특수 문자 등)는 그대로 반환한다.
204///
205/// ```
206/// use cliff3_util::string_util::extract_initial_consonant;
207///
208/// let target = "이건 이모티콘(❤😑😊😂)을 포함합니다.";
209/// let result = extract_initial_consonant(Some(target)).unwrap();
210///
211/// assert_eq!("ㅇㄱ ㅇㅁㅌㅋ(❤😑😊😂)ㅇ ㅍㅎㅎㄴㄷ.", result.as_str());
212/// ```
213///
214/// # Arguments
215///
216/// - `target` 추출 대상 문자열
217///
218/// # Return
219///
220/// - 추출 결과. `Result<String, MissingArgumentError>`
221pub fn extract_initial_consonant(target: Option<&str>) -> Result<String, MissingArgumentError> {
222    match target {
223        None => Err(MissingArgumentError::default()),
224        Some(v) => {
225            let result = {
226                let mut temp = String::with_capacity(v.chars().count()); // 글자수 만큼 미리 생성
227
228                for (_, t) in v.chars().enumerate() {
229                    if t >= '가' && t <= '힣' {
230                        temp += KO_CONSONANTS[(((t as u32) - ('가' as u32)) / 588) as usize]
231                            .to_string()
232                            .as_str();
233                    } else {
234                        temp += t.to_string().as_str();
235                    }
236                }
237
238                temp
239            };
240
241            Ok(result)
242        }
243    }
244}
245
246/// 주어진 문자열에서 한글을 초/중/종성으로 분리.
247///
248/// 초성의 된소리, 중성의 이중모음 및 종성의 겹받침은 분리하지 않는다.
249/// 만약 모든 자음 모음의 완전한 분해가 필요한 경우 [`separate_consonant_vowel_completely`]를 사용한다.
250///
251/// * 초성이 된소리여도 그대로 처리(`ㄲ` -> `ㄲ`, `ㅆ` -> `ㅆ`)
252/// * 중성이 이중 모음이어도 그대로 처리 (`ㅘ` -> `ㅘ`, `ㅙ` ->`ㅙ`)
253/// * 종성이 겹받침이어도 그대로 처리 (`ㄶ` -> `ㄶ`, `ㄺ` -> `ㄺ`)
254///
255/// ```
256/// use cliff3_util::string_util::separate_simple_consonant_vowel;
257///
258/// let mut target = "한글과 English가 함께";
259/// let mut result = separate_simple_consonant_vowel(Some(target)).unwrap();
260///
261/// assert_eq!("ㅎㅏㄴㄱㅡㄹㄱㅘ Englishㄱㅏ ㅎㅏㅁㄲㅔ", result.as_str());
262///
263/// target = "많이 주세요.";
264/// result = separate_simple_consonant_vowel(Some(target)).unwrap();
265///
266/// assert_eq!("ㅁㅏㄶㅇㅣ ㅈㅜㅅㅔㅇㅛ.", result.as_str());
267/// ```
268///
269/// # Arguments
270///
271/// - `target` 추출 대상 문자열
272///
273/// # Return
274///
275/// - 추출 결과. `Result<String, MissingArgumentError>`
276pub fn separate_simple_consonant_vowel(
277    target: Option<&str>,
278) -> Result<String, MissingArgumentError> {
279    match target {
280        None => Err(MissingArgumentError::default()),
281        Some(v) => {
282            let result = {
283                let mut temp = String::with_capacity(v.chars().count() * 3); // 초/중/종성 3개로 분리
284                let mut consonant: u32;
285                let start = '가' as u32;
286
287                for (_, t) in v.chars().enumerate() {
288                    if t >= '가' && t <= '힣' {
289                        consonant = (t as u32) - start;
290
291                        // 초성
292                        temp += KO_CONSONANTS[(consonant / 588) as usize]
293                            .to_string()
294                            .as_str();
295                        consonant = consonant % 588;
296
297                        // 중성
298                        temp += KO_VOWELS[(consonant / 28) as usize].to_string().as_str();
299                        consonant = consonant % 28;
300
301                        if consonant != 0 {
302                            // 종성
303                            temp += KO_FINAL_CONSONANTS[consonant as usize].to_string().as_str();
304                        }
305                    } else {
306                        temp += t.to_string().as_str();
307                    }
308                }
309
310                temp
311            };
312
313            Ok(result)
314        }
315    }
316}
317
318/// 주어진 문자열에서 한글을 초/중/종성으로 완전 분리.
319///
320/// [`separate_simple_consonant_vowel`]과 달리 모든 자음/모음을 완전히 분리한다.
321///
322/// * 초성이 된소리일 경우 분해 (`ㄲ` -> `ㄱㄱ`, `ㅆ` -> `ㅅㅅ`)
323/// * 중성이 이중 모음일 경우 분해 (`ㅘ` -> `ㅗㅏ`, `ㅙ` -> `ㅗㅐ`)
324/// * 종성이 겹받침일 경우 분해 (`ㄶ` -> `ㄴㅎ`, `ㄺ` -> `ㄹㄱ`)
325///
326/// ```
327/// use cliff3_util::string_util::separate_consonant_vowel_completely;
328/// let target = r#""투표율을 40%(percentage) 초중반대는 충분히 되지 않을까 생각한다"며 말문을 뗐다."#;
329/// let result = separate_consonant_vowel_completely(Some(target)).unwrap();
330///
331/// assert_eq!(
332///     r#""ㅌㅜㅍㅛㅇㅠㄹㅇㅡㄹ 40%(percentage) ㅊㅗㅈㅜㅇㅂㅏㄴㄷㅐㄴㅡㄴ ㅊㅜㅇㅂㅜㄴㅎㅣ ㄷㅗㅣㅈㅣ ㅇㅏㄴㅎㅇㅡㄹㄱㄱㅏ ㅅㅐㅇㄱㅏㄱㅎㅏㄴㄷㅏ"ㅁㅕ ㅁㅏㄹㅁㅜㄴㅇㅡㄹ ㄷㄷㅔㅅㅅㄷㅏ."#,
333///     result.as_str(),
334///     "쌍자음, 이중 모음이 있을 경우 분리 실패"
335/// );
336/// ```
337///
338/// # Arguments
339///
340/// - `target` 추출 대상 문자열
341///
342/// # Return
343///
344/// - 추출 결과. `Result<String, MissingArgumentError>`
345pub fn separate_consonant_vowel_completely(
346    target: Option<&str>,
347) -> Result<String, MissingArgumentError> {
348    match target {
349        None => Err(MissingArgumentError::default()),
350        Some(v) => {
351            // 한 글자당 최대 6자가 될 수 있음
352            // 꽊 -> ㄱㄱㅗㅏㄱㄱ
353            let result = {
354                let mut temp = String::with_capacity(v.chars().count() * 6);
355                let mut consonant: u32;
356                let start = '가' as u32;
357
358                for (_, t) in v.chars().enumerate() {
359                    if t >= '가' && t <= '힣' {
360                        consonant = (t as u32) - start;
361
362                        // 초성. 된소리가 포함된 자음을 기준으로 처리
363                        KO_SEPARATED_CONSONANTS[(consonant / 588) as usize]
364                            .iter()
365                            .for_each(|m| {
366                                temp += m.to_string().as_str();
367                            });
368
369                        consonant %= 588;
370
371                        // 중성. 모음 분해 기준으로 처리
372                        KO_SEPARATED_VOWELS[(consonant / 28) as usize]
373                            .iter()
374                            .for_each(|m| {
375                                temp += m.to_string().as_str();
376                            });
377
378                        consonant %= 28;
379
380                        if consonant != 0 {
381                            //종성. 받침 분해 기준으로 처리
382                            KO_SEPARATED_FINAL_CONSONANTS[consonant as usize]
383                                .iter()
384                                .for_each(|m| {
385                                    temp += m.to_string().as_str();
386                                });
387                        }
388                    } else if t >= 'ㄱ' && t <= 'ㅣ' {
389                        // temp += KO_SEPARATED_FORTES_VOWELS[((t as u32) - ('ㄱ' as u32)) as usize]
390                        //     .iter()
391                        //     .collect::<String>()
392                        //     .as_str();
393                        KO_SEPARATED_FORTES_VOWELS[((t as u32) - ('ㄱ' as u32)) as usize]
394                            .iter()
395                            .for_each(|m| {
396                                temp += m.to_string().as_str();
397                            })
398                    } else {
399                        temp += t.to_string().as_str();
400                    }
401                }
402
403                temp
404            };
405
406            Ok(result)
407        }
408    }
409}
410
411/// 대상 슬라이스를 16진수 형태 문자열로 반환.
412///
413/// # Arguments
414///
415/// * `target` - 원본 데이터
416/// * `to_uppercase` - 대/소문자 출력 형태
417///
418/// # Return
419///
420/// - 변환 결과. `Option<Sting>`
421pub fn to_hex(target: Option<&[u8]>, to_uppercase: bool) -> Option<String> {
422    if target.is_none() {
423        return None;
424    }
425
426    let v: Vec<String> = target
427        .unwrap()
428        .iter()
429        .map(|b| {
430            if to_uppercase {
431                format!("{:02X}", b)
432            } else {
433                format!("{:02x}", b)
434            }
435        })
436        .collect();
437
438    return Some(v.join(""));
439}
440
441/// 지정된 길이만큼의 무작위 문자열을 생성
442///
443/// 문자열 원본은 [RANDOM_SOURCE]로 숫자와 알파벳 대/소문자만을 포함한다.
444///
445/// # Arguments
446///
447/// - `length` 생성하고자 하는 문자열의 길이
448///
449/// # Return
450///
451/// - 생성된 문자열
452pub fn generate_random_string(length: u32) -> Option<String> {
453    let mut random = rand::thread_rng();
454    let mut count: u32 = 0;
455    let mut result: Vec<&str> = vec![];
456    let source_size = RANDOM_SOURCE.len() - 1;
457
458    while count < length {
459        let index = random.gen_range(0..=source_size);
460        
461        result.push(RANDOM_SOURCE.get(index).unwrap());
462
463        count += 1;
464    }
465
466    Some(result.join(""))
467}
468
469/// 지정된 길이만큼의 무작위 문자열을 생성
470///
471/// 문자열 원본은 [RANDOM_SOURCE_SPEC]으로 숫자, 알파벳 대/소문자 및 특수문자를 포함한다.
472///
473/// # Arguments
474///
475/// - `length` - 생성하고자 하는 문자열의 길이
476///
477/// # Return
478///
479/// - 생성된 문자열
480pub fn generate_random_string_with_spec(length: u32) -> Option<String> {
481    let mut random = rand::thread_rng();
482    let mut count: u32 = 0;
483    let mut result: Vec<&str> = vec![];
484    let source_size = RANDOM_SOURCE_SPEC.len() - 1;
485
486    while count < length {
487        let index = random.gen_range(0..=source_size);
488        
489        result.push(RANDOM_SOURCE_SPEC.get(index).unwrap());
490
491        count += 1;
492    }
493
494    Some(result.join(""))
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn validate_email_test() {
503        let mut email = "joonho.son@me.com";
504        let result = validate_email(Some(email));
505
506        assert!(!result.is_err());
507        assert!(
508            validate_email(Some(email)).unwrap(),
509            "정상적인 이메일 유효성 검사 실패"
510        );
511
512        email = "test@test";
513
514        assert!(!validate_email(Some(email)).unwrap());
515
516        email = "test@test.";
517
518        assert!(!validate_email(Some(email)).unwrap());
519
520        email = "";
521
522        assert!(!validate_email(Some(email)).unwrap());
523
524        assert!(validate_email(None).is_err());
525
526        // 반환되는 에러가 ValidateError인제 확인
527        assert_eq!(
528            validate_email(None).unwrap_err(),
529            MissingArgumentError::default(),
530            "에러 불일치"
531        );
532    }
533
534    #[test]
535    #[should_panic]
536    fn invalid_email_should_panic_test() {
537        validate_email(None).unwrap();
538    }
539
540    #[test]
541    fn korean_domain_fail_test() {
542        let mut email = "한글ID@test.com";
543
544        assert!(
545            !validate_email(Some(email)).is_err(),
546            "한글 ID를 포함하는 이메일 검사 실패"
547        );
548
549        email = "test@한글도메인.com";
550
551        assert!(
552            !validate_email(Some(email)).is_err(),
553            "한글 도메인을 포함하는 이메일 검사 실패"
554        );
555
556        email = "홍길동@한글도메인.com";
557
558        assert!(
559            !validate_email(Some(email)).is_err(),
560            "한글 ID 및 한글 도메인을 포함하는 이메일 검사 실패"
561        );
562    }
563
564    #[test]
565    fn extract_initial_consonant_test() {
566        let mut target = "한글만 있습니다.";
567        let mut result = extract_initial_consonant(Some(target)).unwrap();
568
569        println!("extract result : {}", result);
570
571        assert_eq!(
572            "ㅎㄱㅁ ㅇㅅㄴㄷ.",
573            result.as_str(),
574            "한글만 있을 경우 초성 추출 실패"
575        );
576
577        target = "한글과 English가 함께 있습니다.";
578        result = extract_initial_consonant(Some(target)).unwrap();
579
580        println!("extract result : {}", result);
581
582        assert_eq!(
583            "ㅎㄱㄱ Englishㄱ ㅎㄲ ㅇㅅㄴㄷ.",
584            result.as_str(),
585            "한글과 영어가 혼재되어 있을 경우 추출 실패"
586        );
587
588        target = "세종대왕(世宗大王)";
589        result = extract_initial_consonant(Some(target)).unwrap();
590
591        println!("extract result : {}", result);
592
593        assert_eq!(
594            "ㅅㅈㄷㅇ(世宗大王)",
595            result.as_str(),
596            "한글과 한자가 혼재되어 있을 경우 추출 실패"
597        );
598
599        target = "이건 이모티콘(❤😑😊😂)을 포함합니다.";
600        result = extract_initial_consonant(Some(target)).unwrap();
601
602        println!("extract result : {}", result);
603
604        assert_eq!(
605            "ㅇㄱ ㅇㅁㅌㅋ(❤😑😊😂)ㅇ ㅍㅎㅎㄴㄷ.",
606            result.as_str(),
607            "한글과 이모티콘이 혼재되어 있을 경우 추출 실패"
608        );
609    }
610
611    #[test]
612    fn separate_consonant_vowel_test() {
613        let mut target = "한글만";
614        let mut result = separate_simple_consonant_vowel(Some(target)).unwrap();
615
616        println!("separate result : {}", result);
617
618        assert_eq!(
619            "ㅎㅏㄴㄱㅡㄹㅁㅏㄴ",
620            result.as_str(),
621            "한글만 있는 초/중/종성 분리 실패"
622        );
623
624        target = "한글과 English가 함께";
625        result = separate_simple_consonant_vowel(Some(target)).unwrap();
626
627        println!("separate result : {}", result);
628
629        assert_eq!(
630            "ㅎㅏㄴㄱㅡㄹㄱㅘ Englishㄱㅏ ㅎㅏㅁㄲㅔ",
631            result.as_str(),
632            "한글과 영어가 혼재되어 있을 경우 초/중/종성 분리 실패"
633        );
634
635        target = "맑음";
636        result = separate_simple_consonant_vowel(Some(target)).unwrap();
637
638        println!("separate result : {}", result);
639
640        assert_eq!(
641            "ㅁㅏㄺㅇㅡㅁ",
642            result.as_str(),
643            "겹받침이 있을 경우 초/중/종성 분리 실패"
644        );
645
646        target = "많이 주세요.";
647        result = separate_simple_consonant_vowel(Some(target)).unwrap();
648
649        println!("separate result : {}", result);
650
651        assert_eq!(
652            "ㅁㅏㄶㅇㅣ ㅈㅜㅅㅔㅇㅛ.",
653            result.as_str(),
654            "겹받침이 있을 경우 초/중/종성 분리 실패"
655        );
656
657        target = "꽊꽊이";
658        result = separate_simple_consonant_vowel(Some(target)).unwrap();
659
660        println!("separate result : {}", result);
661
662        assert_eq!("ㄲㅘㄲㄲㅘㄲㅇㅣ", result.as_str());
663    }
664
665    #[test]
666    fn separate_consonant_vowel_completely_test() {
667        let mut target = "한글만";
668        let mut result = separate_consonant_vowel_completely(Some(target)).unwrap();
669
670        println!("separate result : {}", result);
671
672        assert_eq!(
673            "ㅎㅏㄴㄱㅡㄹㅁㅏㄴ",
674            result.as_str(),
675            "한글만 있는 초/중/종성 분리 실패"
676        );
677
678        target = "꽊꽊이";
679        result = separate_consonant_vowel_completely(Some(target)).unwrap();
680
681        println!("separate result : {}", result);
682
683        assert_eq!(
684            "ㄱㄱㅗㅏㄱㄱㄱㄱㅗㅏㄱㄱㅇㅣ",
685            result.as_str(),
686            "쌍자음, 이중 모음이 있을 경우 분리 실패"
687        );
688
689        target = "꽊많이 줬으면 좋겠어요1❤❤.";
690        result = separate_consonant_vowel_completely(Some(target)).unwrap();
691
692        println!("separate result : {}", result);
693
694        assert_eq!(
695            "ㄱㄱㅗㅏㄱㄱㅁㅏㄴㅎㅇㅣ ㅈㅜㅓㅅㅅㅇㅡㅁㅕㄴ ㅈㅗㅎㄱㅔㅅㅅㅇㅓㅇㅛ1❤❤.",
696            result.as_str(),
697            "쌍자음, 이중모음이 있을 경우 분리 실패"
698        );
699
700        target =
701            r#""투표율을 40%(percentage) 초중반대는 충분히 되지 않을까 생각한다"며 말문을 뗐다."#;
702        result = separate_consonant_vowel_completely(Some(target)).unwrap();
703
704        println!("separate result : {}", result);
705
706        assert_eq!(
707            r#""ㅌㅜㅍㅛㅇㅠㄹㅇㅡㄹ 40%(percentage) ㅊㅗㅈㅜㅇㅂㅏㄴㄷㅐㄴㅡㄴ ㅊㅜㅇㅂㅜㄴㅎㅣ ㄷㅗㅣㅈㅣ ㅇㅏㄴㅎㅇㅡㄹㄱㄱㅏ ㅅㅐㅇㄱㅏㄱㅎㅏㄴㄷㅏ"ㅁㅕ ㅁㅏㄹㅁㅜㄴㅇㅡㄹ ㄷㄷㅔㅅㅅㄷㅏ."#,
708            result.as_str(),
709            "쌍자음, 이중모음, 특수 기호를 포함하는 경우 분리 실패"
710        );
711    }
712
713    #[test]
714    fn random_string_test() {
715        let length = 17;
716        let result = generate_random_string(length);
717
718        assert!(result.is_some());
719
720        let result = result.unwrap();
721
722        assert_eq!(length, result.len() as u32);
723
724        println!(
725            "--------------------------\nrandom string result1: {}--------------------\n",
726            result
727        );
728
729        let length = 38;
730        let result = generate_random_string(length);
731
732        assert!(result.is_some());
733
734        let result = result.unwrap();
735
736        assert_eq!(length, result.len() as u32);
737
738        println!(
739            "--------------------------\nrandom string result2: {}\n--------------------\n",
740            result
741        );
742
743        loop {
744            let result: Option<String> = generate_random_string_with_spec(40);
745
746            if result.is_none() {
747                continue;
748            }
749
750            if result.as_ref().unwrap().contains("!") {
751                break;
752            }
753        }
754    }
755}