1+ using System . Text ;
2+ using System . Collections . Generic ;
3+
4+ namespace FastReport . Functions
5+ {
6+ /// <summary>
7+ /// Converts numbers to Chinese financial notation (大写) for currencies CNY, USD, EUR.
8+ /// </summary>
9+ internal class NumToWordsCn : NumToWordsBase
10+ {
11+ private static Dictionary < string , CurrencyInfo > currencyList ;
12+
13+ private static readonly string Zero = "零" ;
14+
15+ private static readonly string [ ] FixedWords =
16+ {
17+ Zero , "壹" , "贰" , "叁" , "肆" , "伍" , "陆" , "柒" , "捌" , "玖"
18+ } ;
19+
20+ private static readonly string Ten = "拾" ; // 10
21+ private static readonly string Hundred = "佰" ; // 100
22+ private static readonly string Thousand = "仟" ; // 1 000
23+ private static readonly string TenThousand = "万" ; // 10 000
24+ private static readonly string HundredMillion = "亿" ; // 100 000 000
25+
26+ private static readonly string Yuan = "元" ; // Chinese Yuan (CNY)
27+ private static readonly string Jiao = "角" ; // 0.1 Yuan (1/10 of Yuan)
28+ private static readonly string Fen = "分" ; // 0.01 Yuan (1/100 of Yuan)
29+ private static readonly string USD = "美元" ; // US Dollar
30+ private static readonly string USCent = "美分" ; // US Cents
31+ private static readonly string EUR = "欧元" ; // Euro
32+ private static readonly string EuroCent = "欧分" ; // Euro Cents
33+
34+ private static readonly string WholeAmount = "整" ; // "Exact amount" (used for whole numbers without fractional part)
35+ private static readonly string Minus = "负" ;
36+
37+ private static readonly WordInfo unused = new WordInfo ( "" ) ;
38+
39+ private bool _hasNonZeroIntegerPart = false ;
40+
41+ protected override void Str ( long value , WordInfo senior , StringBuilder result )
42+ {
43+ if ( IsFractionalUnit ( senior ) )
44+ {
45+ HandleFractionalPart ( value , senior , result ) ;
46+ }
47+ else
48+ {
49+ HandleIntegerPart ( value , senior , result ) ;
50+ }
51+ }
52+
53+ private void HandleFractionalPart ( long value , WordInfo senior , StringBuilder result )
54+ {
55+ if ( value == 0 )
56+ {
57+ result . Append ( WholeAmount ) ;
58+ return ;
59+ }
60+
61+ if ( senior . one == Fen )
62+ {
63+ HandleCnyFractional ( value , result ) ;
64+ }
65+ else
66+ {
67+ HandleForeignCurrencyFractional ( value , senior , result ) ;
68+ }
69+ }
70+
71+ /// <summary>
72+ /// Handles USD/EUR fractional part as cent amounts.
73+ /// Adds "零" before single-digit cents when integer part exists.
74+ /// </summary>
75+ private void HandleForeignCurrencyFractional ( long value , WordInfo senior , StringBuilder result )
76+ {
77+ string fractionalPart = ConvertInteger ( value ) ;
78+ // Add "零" before single-digit cents if integer part is non-zero
79+ if ( _hasNonZeroIntegerPart && value < 10 )
80+ {
81+ result . Append ( Zero ) ;
82+ }
83+ result . Append ( fractionalPart ) ;
84+
85+ if ( value != 0 )
86+ {
87+ if ( senior . one == USCent ) result . Append ( USCent ) ;
88+ else if ( senior . one == EuroCent ) result . Append ( EuroCent ) ;
89+
90+ }
91+ }
92+
93+ /// <summary>
94+ /// Handles CNY fractional part with 角(jiao)/分(fen) structure.
95+ /// Adds "零" (zero) between integer part and 分 when 角 is missing.
96+ /// </summary>
97+ private void HandleCnyFractional ( long value , StringBuilder result )
98+ {
99+ int jiao = ( int ) ( value / 10 ) ;
100+ int fen = ( int ) ( value % 10 ) ;
101+
102+ if ( jiao > 0 )
103+ result . Append ( FixedWords [ jiao ] ) . Append ( Jiao ) ;
104+
105+ if ( fen > 0 )
106+ {
107+ // Add "零" only if there's no 角 but integer part exists
108+ if ( jiao == 0 && _hasNonZeroIntegerPart )
109+ result . Append ( Zero ) ;
110+ result . Append ( FixedWords [ fen ] ) . Append ( Fen ) ;
111+ }
112+ }
113+
114+ private void HandleIntegerPart ( long value , WordInfo senior , StringBuilder result )
115+ {
116+ _hasNonZeroIntegerPart = ( value != 0 ) ;
117+
118+ string integerPart = ConvertInteger ( value ) ;
119+ if ( value == 0 && string . IsNullOrEmpty ( integerPart ) )
120+ {
121+ integerPart = Zero ;
122+ }
123+ result . Append ( integerPart ) ;
124+
125+ // Add currency symbol
126+ if ( senior . one == Yuan ) result . Append ( Yuan ) ;
127+ else if ( senior . one == USD ) result . Append ( USD ) ;
128+ else if ( senior . one == EUR ) result . Append ( EUR ) ;
129+ }
130+
131+ private bool IsFractionalUnit ( WordInfo unit )
132+ {
133+ return ( unit != null ) &&
134+ ( unit . one == Fen || unit . one == USCent || unit . one == EuroCent ) ;
135+ }
136+
137+ /// <summary>
138+ /// Converts integer number to Chinese financial notation using 亿/万 blocks.
139+ /// </summary>
140+ private string ConvertInteger ( long num )
141+ {
142+ if ( num == 0 ) return "" ;
143+
144+ long units = num % 10000 ;
145+ long tenThousand = ( num / 10000 ) % 10000 ;
146+ long hundredMillion = num / 100000000 ;
147+
148+ StringBuilder sb = new StringBuilder ( ) ;
149+
150+ // 亿
151+ if ( hundredMillion > 0 )
152+ {
153+ sb . Append ( ConvertFourDigits ( ( int ) hundredMillion ) ) ;
154+ sb . Append ( HundredMillion ) ;
155+ }
156+
157+ // 万
158+ if ( tenThousand > 0 )
159+ {
160+ // Missing digits between 亿 and 万 blocks
161+ if ( hundredMillion > 0 && units > 0 && tenThousand < 1000 )
162+ {
163+ if ( ! sb . ToString ( ) . EndsWith ( Zero ) )
164+ sb . Append ( Zero ) ;
165+ }
166+ sb . Append ( ConvertFourDigits ( ( int ) tenThousand ) ) ;
167+ sb . Append ( TenThousand ) ;
168+ }
169+ else if ( hundredMillion > 0 && units > 0 )
170+ {
171+ sb . Append ( Zero ) ;
172+ }
173+
174+ if ( units > 0 )
175+ {
176+ string unitStr = ConvertFourDigits ( ( int ) units ) ;
177+ // Missing thousands after 万 block
178+ if ( tenThousand > 0 && units < 1000 && ! unitStr . StartsWith ( Zero ) )
179+ {
180+ unitStr = Zero + unitStr ;
181+ }
182+ sb . Append ( unitStr ) ;
183+ }
184+
185+ return sb . ToString ( ) ;
186+ }
187+
188+ /// <summary>
189+ /// Converts 4-digit number to Chinese financial notation.
190+ /// </summary>
191+ private string ConvertFourDigits ( int num )
192+ {
193+ if ( num == 0 ) return "" ;
194+ StringBuilder sb = new StringBuilder ( ) ;
195+
196+ bool hasThousands = false ;
197+ bool hasHundreds = false ;
198+
199+ if ( num >= 1000 )
200+ {
201+ sb . Append ( FixedWords [ num / 1000 ] ) . Append ( Thousand ) ;
202+ num %= 1000 ;
203+ hasThousands = true ;
204+ }
205+
206+ if ( num >= 100 )
207+ {
208+ sb . Append ( FixedWords [ num / 100 ] ) . Append ( Hundred ) ;
209+ num %= 100 ;
210+ hasHundreds = true ;
211+ }
212+ else if ( ( hasThousands ) & & num > 0 )
213+ {
214+ // Missing hundreds between thousands and lower digits
215+ sb . Append ( Zero ) ;
216+ }
217+
218+ if ( num >= 10 )
219+ {
220+ sb . Append ( FixedWords [ num / 10 ] ) . Append ( Ten ) ;
221+ num %= 10 ;
222+ }
223+ else if ( ( hasThousands || hasHundreds ) && num > 0 )
224+ {
225+ // Missing tens between higher digits and units
226+ if ( ! sb . ToString ( ) . EndsWith ( Zero ) )
227+ sb . Append ( Zero ) ;
228+ }
229+
230+ if ( num > 0 )
231+ {
232+ sb . Append ( FixedWords [ num ] ) ;
233+ }
234+
235+ return sb . ToString ( ) ;
236+ }
237+
238+ protected override int GetFixedWordsCount ( ) => 10 ;
239+ protected override string GetFixedWords ( bool male , long value ) =>
240+ ( value >= 0 && value < FixedWords . Length ) ? FixedWords [ value ] : string . Empty ;
241+ protected override string GetTen ( bool male , long value ) => string . Empty ;
242+ protected override string GetHund ( bool male , long value ) => string . Empty ;
243+
244+ protected override WordInfo GetThousands ( ) => unused ;
245+ protected override WordInfo GetMillions ( ) => unused ;
246+ protected override WordInfo GetMilliards ( ) => unused ;
247+ protected override WordInfo GetTrillions ( ) => unused ;
248+
249+ protected override CurrencyInfo GetCurrency ( string currencyName )
250+ {
251+ currencyName = currencyName . ToUpper ( ) ;
252+ if ( currencyList . TryGetValue ( currencyName , out var info ) )
253+ return info ;
254+ return currencyList [ "CNY" ] ;
255+ }
256+
257+ protected override string GetZero ( ) => Zero ;
258+ protected override string GetMinus ( ) => Minus ;
259+ protected override string GetDecimalSeparator ( ) => string . Empty ;
260+ protected override string Get10_1Separator ( ) => string . Empty ;
261+ protected override string Get100_10Separator ( ) => string . Empty ;
262+ protected override string Case ( long value , WordInfo info ) => info . one ;
263+
264+ static NumToWordsCn ( )
265+ {
266+ currencyList = new Dictionary < string , CurrencyInfo >
267+ {
268+ [ "CNY" ] = new CurrencyInfo ( new WordInfo ( Yuan ) , new WordInfo ( Fen ) ) ,
269+ [ "USD" ] = new CurrencyInfo ( new WordInfo ( USD ) , new WordInfo ( USCent ) ) ,
270+ [ "EUR" ] = new CurrencyInfo ( new WordInfo ( EUR ) , new WordInfo ( EuroCent ) )
271+ } ;
272+ }
273+ }
274+ }
0 commit comments