1+ use unicode_normalization:: UnicodeNormalization ;
2+
3+ const FRACTION_SLASH : char = '\u{2044}' ;
4+
5+ fn consume_whitespace ( iter : & mut std:: iter:: Peekable < std:: str:: Chars > ) {
6+ while let Some ( c) = iter. peek ( ) {
7+ if c. is_whitespace ( ) {
8+ iter. next ( ) ;
9+ } else {
10+ break ;
11+ }
12+ }
13+ }
14+
15+ fn encode_number_string ( s : & str , part_name : & str ) -> Result < Vec < u8 > , String > {
16+ let mut result = Vec :: new ( ) ;
17+ for c in s. chars ( ) {
18+ if !c. is_ascii_digit ( ) {
19+ return Err ( format ! ( "Invalid {} part (non-ascii digit): {}" , part_name, c) ) ;
20+ }
21+ result. extend ( crate :: number:: encode_number ( c) ) ;
22+ }
23+ Ok ( result)
24+ }
25+
26+ pub fn encode_fraction ( numerator : & str , denominator : & str ) -> Result < Vec < u8 > , String > {
27+ let mut result = vec ! [ 60 ] ;
28+ result. extend ( encode_number_string ( denominator, "fraction denominator" ) ?) ;
29+ result. push ( 12 ) ;
30+ result. push ( 60 ) ;
31+ result. extend ( encode_number_string ( numerator, "fraction numerator" ) ?) ;
32+ Ok ( result)
33+ }
34+
35+ pub fn encode_fraction_in_context ( numerator : & str , denominator : & str ) -> Result < Vec < u8 > , String > {
36+ let mut result = vec ! [ 60 ] ;
37+ result. extend ( encode_number_string ( numerator, "fraction numerator" ) ?) ;
38+ result. push ( 56 ) ;
39+ result. push ( 12 ) ;
40+ result. push ( 60 ) ;
41+ result. extend ( encode_number_string ( denominator, "fraction denominator" ) ?) ;
42+ Ok ( result)
43+ }
44+
45+ pub fn encode_mixed_fraction ( whole : & str , numerator : & str , denominator : & str ) -> Result < Vec < u8 > , String > {
46+ let mut result = vec ! [ 60 ] ;
47+ result. extend ( encode_number_string ( whole, "whole number" ) ?) ;
48+ result. extend ( encode_fraction ( numerator, denominator) ?) ;
49+ Ok ( result)
50+ }
51+
52+ fn normalize_digit ( c : char ) -> Option < char > {
53+ match c {
54+ '0' | '⁰' | '₀' => Some ( '0' ) ,
55+ '1' | '¹' | '₁' => Some ( '1' ) ,
56+ '2' | '²' | '₂' => Some ( '2' ) ,
57+ '3' | '³' | '₃' => Some ( '3' ) ,
58+ '4' | '⁴' | '₄' => Some ( '4' ) ,
59+ '5' | '⁵' | '₅' => Some ( '5' ) ,
60+ '6' | '⁶' | '₆' => Some ( '6' ) ,
61+ '7' | '⁷' | '₇' => Some ( '7' ) ,
62+ '8' | '⁸' | '₈' => Some ( '8' ) ,
63+ '9' | '⁹' | '₉' => Some ( '9' ) ,
64+ _ => None ,
65+ }
66+ }
67+
68+ fn read_braced_content (
69+ iter : & mut std:: iter:: Peekable < std:: str:: Chars >
70+ ) -> Option < String > {
71+ consume_whitespace ( iter) ;
72+
73+ if iter. next ( ) ? != '{' { return None ; }
74+
75+ let mut content = String :: new ( ) ;
76+ while let Some ( c) = iter. peek ( ) {
77+ match c {
78+ '}' => {
79+ iter. next ( ) ;
80+ return if content. is_empty ( ) { None } else { Some ( content) } ;
81+ }
82+ _ if c. is_whitespace ( ) => {
83+ iter. next ( ) ;
84+ }
85+ _ => {
86+ if let Some ( digit) = normalize_digit ( * c) {
87+ content. push ( digit) ;
88+ iter. next ( ) ;
89+ } else {
90+ return None ;
91+ }
92+ }
93+ }
94+ }
95+ None
96+ }
97+
98+ pub fn parse_latex_fraction ( s : & str ) -> Option < ( Option < String > , String , String ) > {
99+ let mut iter = s. trim ( ) . chars ( ) . peekable ( ) ;
100+
101+ if iter. next ( ) ? != '$' { return None ; }
102+
103+ consume_whitespace ( & mut iter) ;
104+
105+ let mut whole_part_str = String :: new ( ) ;
106+ while let Some ( digit) = iter. peek ( ) . and_then ( |c| normalize_digit ( * c) ) {
107+ whole_part_str. push ( digit) ;
108+ iter. next ( ) ;
109+ }
110+ let whole_part = if whole_part_str. is_empty ( ) { None } else { Some ( whole_part_str) } ;
111+
112+ consume_whitespace ( & mut iter) ;
113+
114+ if iter. next ( ) != Some ( '\\' ) ||
115+ iter. next ( ) != Some ( 'f' ) ||
116+ iter. next ( ) != Some ( 'r' ) ||
117+ iter. next ( ) != Some ( 'a' ) ||
118+ iter. next ( ) != Some ( 'c' ) {
119+ return None ;
120+ }
121+
122+ let numerator = read_braced_content ( & mut iter) ?;
123+ let denominator = read_braced_content ( & mut iter) ?;
124+
125+ consume_whitespace ( & mut iter) ;
126+
127+ if iter. next ( ) ? != '$' { return None ; }
128+
129+ consume_whitespace ( & mut iter) ;
130+
131+ if iter. next ( ) . is_some ( ) {
132+ return None ;
133+ }
134+
135+ Some ( ( whole_part, numerator, denominator) )
136+ }
137+
138+ pub fn parse_unicode_fraction ( c : char ) -> Option < ( String , String ) > {
139+ let decomposed = c. nfkd ( ) . collect :: < String > ( ) ;
140+ if !decomposed. contains ( FRACTION_SLASH ) {
141+ return None ;
142+ }
143+
144+ let parts: Vec < & str > = decomposed. split ( FRACTION_SLASH ) . collect ( ) ;
145+
146+ if parts. len ( ) == 2 {
147+ let num_str = parts[ 0 ] . trim ( ) ;
148+ let den_str = parts[ 1 ] . trim ( ) ;
149+ if num_str. is_empty ( ) || den_str. is_empty ( ) {
150+ return None ;
151+ }
152+ if !num_str. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) {
153+ return None ;
154+ }
155+ if !den_str. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) {
156+ return None ;
157+ }
158+ Some ( ( num_str. to_string ( ) , den_str. to_string ( ) ) )
159+ } else {
160+ None
161+ }
162+ }
163+
164+ pub fn is_unicode_fraction ( c : char ) -> bool {
165+ parse_unicode_fraction ( c) . is_some ( )
166+ }
0 commit comments