1use crate::_private::NonExhaustive;
10use rat_event::{HandleEvent, MouseOnly, Regular};
11use rat_text::date_input::DateInputState;
12use rat_text::event::{ReadOnly, TextOutcome};
13use rat_text::text_input_mask::{MaskedInput, MaskedInputState};
14use rat_text::{
15 TextError, TextFocusGained, TextFocusLost, TextStyle, TextTab, derive_text_widget,
16 derive_text_widget_state,
17};
18use ratatui_core::buffer::Buffer;
19use ratatui_core::layout::Rect;
20use ratatui_core::style::Style;
21use ratatui_core::widgets::StatefulWidget;
22use ratatui_crossterm::crossterm::event::Event;
23use std::cmp::min;
24use std::str::FromStr;
25
26#[derive(Debug, Default, Clone)]
28pub struct IBANInput<'a> {
29 widget: MaskedInput<'a>,
30}
31
32#[derive(Debug, Clone)]
34pub struct IBANInputState {
35 pub area: Rect,
37 pub inner: Rect,
39 pub widget: MaskedInputState,
41
42 pub country: String,
44
45 pub non_exhaustive: NonExhaustive,
46}
47
48impl<'a> IBANInput<'a> {
49 pub fn new() -> Self {
50 Self::default()
51 }
52}
53
54derive_text_widget!(IBANInput<'a>);
55
56impl<'a> StatefulWidget for &IBANInput<'a> {
57 type State = IBANInputState;
58
59 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
60 (&self.widget).render(area, buf, &mut state.widget);
61
62 state.area = state.widget.area;
63 state.inner = state.widget.inner;
64 }
65}
66
67impl StatefulWidget for IBANInput<'_> {
68 type State = IBANInputState;
69
70 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
71 self.widget.render(area, buf, &mut state.widget);
72
73 state.area = state.widget.area;
74 state.inner = state.widget.inner;
75 }
76}
77
78impl Default for IBANInputState {
79 fn default() -> Self {
80 let mut z = Self {
81 area: Default::default(),
82 inner: Default::default(),
83 widget: Default::default(),
84 country: Default::default(),
85 non_exhaustive: NonExhaustive,
86 };
87 _ = z.widget.set_mask("ll");
88 z
89 }
90}
91
92derive_text_widget_state!(IBANInputState);
93
94impl IBANInputState {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn named(name: &str) -> Self {
101 let mut z = Self::default();
102 z.widget.focus = z.widget.focus.with_name(name);
103 z
104 }
105
106 #[inline]
108 pub fn clear(&mut self) {
109 self.widget.clear();
110 }
111
112 pub fn validate(&mut self) -> Result<bool, TextError> {
116 let iban = from_display_format(&self.country, self.widget.text());
117
118 if self.country.as_str() != country_code(&iban) {
119 let cursor = self.widget.cursor();
120 let anchor = self.widget.anchor();
121 self.widget.set_mask(pattern(&iban)).expect("valid_mask");
122 self.country = country_code(&iban).to_string();
123 self.widget.set_text(to_display_format(&iban));
124 self.widget.set_selection(anchor, cursor);
125 }
126
127 let r = if !iban.trim().is_empty() {
128 valid_iban_country(&iban) && valid_check_sum(&iban)
129 } else {
130 true
131 };
132
133 self.widget.set_invalid(!r);
134 Ok(r)
135 }
136
137 pub fn set_value(&mut self, iban: impl AsRef<str>) {
145 let iban = iban.as_ref();
146 self.country = country_code(iban).to_string();
147 self.widget.set_mask(pattern(iban)).expect("valid mask");
148 self.widget.set_text(to_display_format(iban));
149 let valid = if !iban.trim().is_empty() {
150 valid_iban_country(&iban) && valid_check_sum(&iban)
151 } else {
152 true
153 };
154 self.widget.set_invalid(!valid);
155 }
156
157 pub fn value(&self) -> String {
161 let txt = self.widget.text();
162 from_display_format(&self.country, txt)
163 }
164
165 pub fn valid_value(&self) -> Result<String, TextError> {
169 let txt = self.widget.text();
170 let iban = from_display_format(&self.country, txt);
171 if iban.trim().is_empty() {
172 Ok(Default::default())
173 } else if is_valid_iban(&iban) {
174 Ok(iban)
175 } else {
176 Err(TextError::InvalidValue)
177 }
178 }
179}
180
181impl HandleEvent<Event, Regular, TextOutcome> for IBANInputState {
182 fn handle(&mut self, event: &Event, _keymap: Regular) -> TextOutcome {
183 match self.widget.handle(event, Regular) {
184 TextOutcome::TextChanged => {
185 if let Err(_) = self.validate() {
186 self.set_invalid(true);
187 }
188 TextOutcome::TextChanged
189 }
190 r => r,
191 }
192 }
193}
194
195impl HandleEvent<Event, ReadOnly, TextOutcome> for IBANInputState {
196 fn handle(&mut self, event: &Event, _keymap: ReadOnly) -> TextOutcome {
197 self.widget.handle(event, ReadOnly)
198 }
199}
200
201impl HandleEvent<Event, MouseOnly, TextOutcome> for IBANInputState {
202 fn handle(&mut self, event: &Event, _keymap: MouseOnly) -> TextOutcome {
203 self.widget.handle(event, MouseOnly)
204 }
205}
206
207pub fn handle_events(state: &mut IBANInputState, focus: bool, event: &Event) -> TextOutcome {
211 state.widget.focus.set(focus);
212 state.handle(event, Regular)
213}
214
215pub fn handle_readonly_events(
219 state: &mut IBANInputState,
220 focus: bool,
221 event: &Event,
222) -> TextOutcome {
223 state.widget.focus.set(focus);
224 state.handle(event, ReadOnly)
225}
226
227pub fn handle_mouse_events(state: &mut DateInputState, event: &Event) -> TextOutcome {
229 state.handle(event, MouseOnly)
230}
231
232pub fn is_valid_iban(iban: &str) -> bool {
237 if !valid_iban_country(iban) {
238 return false;
239 }
240 if iban_len(iban) != Some(iban.len() as u8) {
241 return false;
242 }
243 if !valid_check_sum(iban) {
244 return false;
245 }
246 true
247}
248
249static IBAN: &'static [(&'static str, u8)] = &[
250 ("AD", 24),
251 ("AE", 23),
252 ("AL", 28),
253 ("AT", 20),
254 ("AZ", 28),
255 ("BA", 20),
256 ("BE", 16),
257 ("BG", 22),
258 ("BH", 22),
259 ("BR", 29),
260 ("BY", 28),
261 ("CH", 21),
262 ("CR", 22),
263 ("CY", 28),
264 ("CZ", 24),
265 ("DE", 22),
266 ("DK", 18),
267 ("DO", 28),
268 ("EE", 20),
269 ("EG", 29),
270 ("ES", 24),
271 ("FI", 18),
272 ("FO", 18),
273 ("FR", 27),
274 ("GB", 22),
275 ("GE", 22),
276 ("GI", 23),
277 ("GL", 18),
278 ("GR", 27),
279 ("GT", 28),
280 ("HR", 21),
281 ("HU", 28),
282 ("IE", 22),
283 ("IL", 23),
284 ("IQ", 23),
285 ("IS", 26),
286 ("IT", 27),
287 ("JO", 30),
288 ("KW", 30),
289 ("KZ", 20),
290 ("LB", 28),
291 ("LC", 32),
292 ("LI", 21),
293 ("LT", 20),
294 ("LU", 20),
295 ("LV", 21),
296 ("MC", 27),
297 ("MD", 24),
298 ("ME", 22),
299 ("MK", 19),
300 ("MR", 27),
301 ("MT", 31),
302 ("MU", 30),
303 ("NL", 18),
304 ("NO", 15),
305 ("PK", 24),
306 ("PL", 28),
307 ("PS", 29),
308 ("PT", 25),
309 ("QA", 29),
310 ("RO", 24),
311 ("RS", 22),
312 ("SA", 24),
313 ("SC", 31),
314 ("SE", 24),
315 ("SI", 19),
316 ("SK", 24),
317 ("SM", 27),
318 ("ST", 25),
319 ("SV", 28),
320 ("TL", 23),
321 ("TN", 24),
322 ("TR", 26),
323 ("UA", 29),
324 ("VG", 24),
325 ("XK", 20),
326];
327
328fn country_code(iban: &str) -> &str {
329 let mut cit = iban.char_indices();
330 let mut c_end = 0;
331 cit.next();
332 if let Some(c) = cit.next() {
333 c_end = c.0 + c.1.len_utf8();
334 }
335 &iban[0..c_end]
336}
337
338fn enc(c: char, buf: &mut String) -> bool {
339 match c {
340 '0'..='9' => buf.push(c),
341 'a'..='z' => buf.push_str(format!("{}", (c as u32 - 'a' as u32) + 10).as_str()),
342 'A'..='Z' => buf.push_str(format!("{}", (c as u32 - 'A' as u32) + 10).as_str()),
343 ' ' => { }
344 _ => return false,
345 }
346 true
347}
348
349fn valid_check_sum(iban: &str) -> bool {
350 let mut cit = iban.chars();
351 let Some(c0) = cit.next() else {
352 return false;
353 };
354 let Some(c1) = cit.next() else {
355 return false;
356 };
357 let Some(c2) = cit.next() else {
358 return false;
359 };
360 let Some(c3) = cit.next() else {
361 return false;
362 };
363
364 let mut buf = String::new();
365 for c in cit {
366 if !enc(c, &mut buf) {
367 return false;
368 }
369 }
370 if !enc(c0, &mut buf) {
371 return false;
372 }
373 if !enc(c1, &mut buf) {
374 return false;
375 }
376 if !enc(c2, &mut buf) {
377 return false;
378 }
379 if !enc(c3, &mut buf) {
380 return false;
381 }
382 let buf = buf.as_str();
383
384 let mut c0 = 0;
385 let mut c1 = min(buf.len(), 18);
386 let mut r = 0;
387 loop {
388 let mut v = u64::from_str(&buf[c0..c1]).expect("integer");
389
390 v += r * 10u64.pow(v.ilog10() + 1);
391 r = v % 97;
392
393 c0 = c1;
394 c1 = min(buf.len(), c1 + 18);
395
396 if c0 == c1 {
397 break;
398 }
399 }
400
401 r == 1
402}
403
404fn valid_iban_country(iban: &str) -> bool {
405 let cc = country_code(iban);
406 for v in IBAN {
407 if v.0 == cc {
408 return true;
409 }
410 }
411 false
412}
413
414fn iban_len(iban: &str) -> Option<u8> {
417 let cc = country_code(iban);
418 for v in IBAN {
419 if v.0 == cc {
420 return Some(v.1);
421 }
422 }
423 None
424}
425
426fn pattern(iban: &str) -> String {
427 if let Some(len) = iban_len(iban) {
428 let mut buf = String::new();
429 buf.push_str("lldd ");
430 for i in 0..len - 4 {
431 if i > 0 && i % 4 == 0 {
432 buf.push(' ');
433 }
434 buf.push('a');
435 }
436 buf
437 } else {
438 "___________________________________".to_string()
439 }
440}
441
442fn to_display_format(iban: &str) -> String {
443 if valid_iban_country(iban) {
444 let mut buf = String::new();
445 for (i, c) in iban.chars().enumerate() {
446 if i > 0 && i % 4 == 0 {
447 buf.push(' ');
448 }
449 buf.push(c);
450 }
451 buf
452 } else {
453 iban.to_string()
454 }
455}
456
457fn from_display_format(cc: &str, iban: &str) -> String {
458 if valid_iban_country(cc) {
459 let mut buf = String::new();
460 for (i, c) in iban.chars().enumerate() {
461 if (i + 1) % 5 != 0 {
462 buf.push(c);
463 }
464 }
465 buf
466 } else {
467 iban.to_string()
468 }
469}