Skip to content

Commit c8d5868

Browse files
authored
Merge pull request #90 from ihyeeun/refactor/braille-rules-32-35
[Refactor] 로마자 구간을 열고 닫을 때 규칙 로직 구현 및 rule_32 테스트 케이스 추가
2 parents 4f1c478 + 0211b31 commit c8d5868

6 files changed

Lines changed: 661 additions & 44 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
use crate::{symbol_shortcut, utils};
2+
3+
/// 규칙 33~35에서 종료표(⠲)를 생략해야 하는 기호 모음.
4+
/// 기호 앞뒤에서는 로마자 종료표를 생략한다.
5+
pub(crate) fn should_skip_terminator_for_symbol(symbol: char) -> bool {
6+
matches!(symbol, '.' | '?'| '!'| '…'| '⋯'| '"'| '\''| '”'| '’'| '」'| '』'
7+
| '〉'| '》'| '('| ')'| ']'| '}'| ','| ':'| ';'| '―')
8+
}
9+
10+
/// 종료표를 생략한 뒤에도 연속표(⠐)로 이어야 하는 기호 모음.
11+
/// 여는 괄호 '(' 는 새 영어 구간을 열게 되므로 제외한다.
12+
/// 종료표를 생략했지만 이어지는 로마자에 연속표를 붙여야 하는지 판단한다.
13+
pub(crate) fn should_request_continuation(symbol: char) -> bool {
14+
matches!(symbol, '.' | '?'| '!'| '…'| '⋯'| '"'| '\''| '”'| '’'| '」'| '』'| '〉'
15+
| '》'| ')'| ']'| '}'| ','| ':'| ';'| '―')
16+
}
17+
18+
/// 제33항 [다만] : '/', '-', '~' 앞에는 종료표를 강제로 붙인다.
19+
pub(crate) fn should_force_terminator_before_symbol(symbol: char) -> bool {
20+
matches!(symbol, '/' | '-' | '~' | '∼')
21+
}
22+
23+
/// 영어 점자 전용 기호인지 확인.[외국어 점자 일람표의 문장 부호 참고]
24+
pub(crate) fn is_english_symbol(symbol: char) -> bool {
25+
symbol_shortcut::is_english_symbol_char(symbol)
26+
}
27+
28+
/// 단일 소문자 단어가 연속될 때 연속표가 필요한지 판단한다.
29+
/// [통일 영어 점자 - 5.2 1급 점자 기호표(⠰)] : 글자 a, i, o 앞에는 1급 점자 기호표가 필요하지 않다.
30+
pub(crate) fn requires_single_letter_continuation(letter: char) -> bool {
31+
letter.is_ascii_lowercase() && !matches!(letter, 'a' | 'i' | 'o')
32+
}
33+
34+
fn is_ascii_letter_or_digit(ch: Option<char>) -> bool {
35+
ch.is_some_and(|c| c.is_ascii_alphanumeric())
36+
}
37+
38+
pub(crate) fn prev_ascii_letter_or_digit(word_chars: &[char], index: usize) -> bool {
39+
let mut j = index;
40+
while j > 0 {
41+
let ch = word_chars[j - 1];
42+
if ch.is_ascii_alphanumeric() {
43+
return true;
44+
}
45+
if symbol_shortcut::is_english_symbol_char(ch) {
46+
j -= 1;
47+
continue;
48+
}
49+
break;
50+
}
51+
false
52+
}
53+
54+
pub(crate) fn next_ascii_letter_or_digit(
55+
word_chars: &[char],
56+
index: usize,
57+
remaining_words: &[&str],
58+
) -> bool {
59+
let mut j = index + 1;
60+
while j < word_chars.len() {
61+
let ch = word_chars[j];
62+
if ch.is_ascii_alphanumeric() {
63+
return true;
64+
}
65+
if symbol_shortcut::is_english_symbol_char(ch) {
66+
j += 1;
67+
continue;
68+
}
69+
return false;
70+
}
71+
72+
for word in remaining_words {
73+
for ch in word.chars() {
74+
if ch.is_ascii_alphanumeric() {
75+
return true;
76+
}
77+
if symbol_shortcut::is_english_symbol_char(ch) {
78+
continue;
79+
}
80+
return false;
81+
}
82+
}
83+
84+
false
85+
}
86+
87+
#[allow(clippy::too_many_arguments)]
88+
/// 괄호/쉼표가 영어 점자로 이어져야 하는지 판정한다.
89+
/// - '(' 는 뒤에 올 문자가 ASCII 영숫자여야 하고, 앞은 한글이 아니어야 한다.
90+
/// - ')' 는 여는 괄호가 영어 기호로 열렸던 경우에만 영어 기호로 닫는다.
91+
/// - ',' 는 앞뒤 모두 ASCII 영숫자가 이어지는 경우에만 영어 점자로 유지한다.
92+
pub(crate) fn should_render_symbol_as_english(
93+
english_indicator: bool,
94+
is_english: bool,
95+
parenthesis_stack: &[bool],
96+
symbol: char,
97+
word_chars: &[char],
98+
index: usize,
99+
remaining_words: &[&str],
100+
) -> bool {
101+
if !english_indicator {
102+
return false;
103+
}
104+
105+
let prev_char = if index > 0 {
106+
Some(word_chars[index - 1])
107+
} else {
108+
None
109+
};
110+
let next_char = if index + 1 < word_chars.len() {
111+
Some(word_chars[index + 1])
112+
} else {
113+
remaining_words.first().and_then(|w| w.chars().next())
114+
};
115+
116+
match symbol {
117+
'(' => is_ascii_letter_or_digit(next_char) && !prev_char.is_some_and(utils::is_korean_char),
118+
')' => parenthesis_stack.last().copied().unwrap_or(false),
119+
',' => {
120+
if !is_english {
121+
return false;
122+
}
123+
124+
let prev_ascii = prev_ascii_letter_or_digit(word_chars, index);
125+
let next_ascii = next_ascii_letter_or_digit(word_chars, index, remaining_words);
126+
127+
prev_ascii && next_ascii
128+
}
129+
_ => false,
130+
}
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
137+
#[test]
138+
fn requires_single_letter_continuation_distinguishes_letters() {
139+
assert!(requires_single_letter_continuation('b'));
140+
assert!(!requires_single_letter_continuation('a'));
141+
assert!(!requires_single_letter_continuation('A'));
142+
}
143+
144+
#[test]
145+
fn skip_and_force_terminator_sets_are_separate() {
146+
for symbol in ['.', '?', '!', ')', ']', ','] {
147+
assert!(should_skip_terminator_for_symbol(symbol));
148+
}
149+
for symbol in ['/', '-', '~'] {
150+
assert!(should_force_terminator_before_symbol(symbol));
151+
assert!(!should_skip_terminator_for_symbol(symbol));
152+
}
153+
assert!(should_request_continuation('.'));
154+
assert!(!should_request_continuation('('));
155+
}
156+
157+
#[test]
158+
fn english_symbol_detection_matches_lookup_table() {
159+
assert!(is_english_symbol('('));
160+
assert!(is_english_symbol(')'));
161+
assert!(is_english_symbol(','));
162+
assert!(!is_english_symbol('?'));
163+
}
164+
165+
#[test]
166+
fn prev_ascii_letter_or_digit_skips_english_symbols() {
167+
let word: Vec<char> = "A(,B".chars().collect();
168+
assert!(prev_ascii_letter_or_digit(&word, 2));
169+
170+
let hangul: Vec<char> = "가,".chars().collect();
171+
assert!(!prev_ascii_letter_or_digit(&hangul, 1));
172+
}
173+
174+
#[test]
175+
fn next_ascii_letter_or_digit_checks_future_ascii() {
176+
let contiguous: Vec<char> = "A,B".chars().collect();
177+
assert!(next_ascii_letter_or_digit(&contiguous, 1, &[]));
178+
179+
let with_symbol: Vec<char> = "A,(B".chars().collect();
180+
assert!(next_ascii_letter_or_digit(&with_symbol, 1, &[]));
181+
182+
let with_remaining: Vec<char> = "A,".chars().collect();
183+
assert!(next_ascii_letter_or_digit(&with_remaining, 1, &["B"]));
184+
185+
let hangul: Vec<char> = "A,가".chars().collect();
186+
assert!(!next_ascii_letter_or_digit(&hangul, 1, &[]));
187+
188+
let only_symbols: Vec<char> = "A,".chars().collect();
189+
assert!(next_ascii_letter_or_digit(&only_symbols, 1, &["(B"]));
190+
assert!(!next_ascii_letter_or_digit(&only_symbols, 1, &["()"]));
191+
}
192+
193+
#[test]
194+
fn should_render_symbol_as_english_for_parentheses() {
195+
let opener: Vec<char> = "(Hello".chars().collect();
196+
assert!(should_render_symbol_as_english(
197+
true, false, &[], '(', &opener, 0, &[]
198+
));
199+
200+
let korean_before: Vec<char> = "가(".chars().collect();
201+
assert!(!should_render_symbol_as_english(
202+
true,
203+
false,
204+
&[],
205+
'(',
206+
&korean_before,
207+
1,
208+
&["A"]
209+
));
210+
211+
assert!(!should_render_symbol_as_english(
212+
false, false, &[], '(', &opener, 0, &[]
213+
));
214+
}
215+
216+
#[test]
217+
fn should_render_symbol_as_english_for_closing_parenthesis() {
218+
let closer: Vec<char> = ")".chars().collect();
219+
assert!(should_render_symbol_as_english(
220+
true,
221+
true,
222+
&[true],
223+
')',
224+
&closer,
225+
0,
226+
&[]
227+
));
228+
229+
assert!(!should_render_symbol_as_english(
230+
true,
231+
true,
232+
&[false],
233+
')',
234+
&closer,
235+
0,
236+
&[]
237+
));
238+
}
239+
240+
#[test]
241+
fn should_render_symbol_as_english_for_comma_requires_ascii_neighbors() {
242+
let word: Vec<char> = "A,B".chars().collect();
243+
assert!(should_render_symbol_as_english(
244+
true,
245+
true,
246+
&[],
247+
',',
248+
&word,
249+
1,
250+
&[]
251+
));
252+
253+
let no_english_context: Vec<char> = "A,B".chars().collect();
254+
assert!(!should_render_symbol_as_english(
255+
true,
256+
false,
257+
&[],
258+
',',
259+
&no_english_context,
260+
1,
261+
&[]
262+
));
263+
264+
let korean_neighbor: Vec<char> = "가,B".chars().collect();
265+
assert!(!should_render_symbol_as_english(
266+
true,
267+
true,
268+
&[],
269+
',',
270+
&korean_neighbor,
271+
1,
272+
&[]
273+
));
274+
}
275+
}

0 commit comments

Comments
 (0)