1use crate::value::StrykeValue;
18
19#[inline]
20fn b(v: bool) -> StrykeValue {
21 StrykeValue::integer(if v { 1 } else { 0 })
22}
23
24fn arg_str(args: &[StrykeValue]) -> String {
25 args.first().map(|v| v.to_string()).unwrap_or_default()
26}
27
28pub fn is_alpha_only(args: &[StrykeValue]) -> StrykeValue {
33 let s = arg_str(args);
34 b(!s.is_empty() && s.chars().all(|c| c.is_ascii_alphabetic()))
35}
36
37pub fn is_alphanumeric_only(args: &[StrykeValue]) -> StrykeValue {
38 let s = arg_str(args);
39 b(!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()))
40}
41
42pub fn is_numeric_only(args: &[StrykeValue]) -> StrykeValue {
43 let s = arg_str(args);
44 b(!s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
45}
46
47pub fn is_ascii_only(args: &[StrykeValue]) -> StrykeValue {
48 b(arg_str(args).is_ascii())
49}
50
51pub fn is_printable_ascii(args: &[StrykeValue]) -> StrykeValue {
52 let s = arg_str(args);
53 b(!s.is_empty() && s.chars().all(|c| c.is_ascii() && !c.is_ascii_control()))
54}
55
56pub fn is_utf8(args: &[StrykeValue]) -> StrykeValue {
57 let s = arg_str(args);
61 b(std::str::from_utf8(s.as_bytes()).is_ok())
62}
63
64pub fn is_lowercase(args: &[StrykeValue]) -> StrykeValue {
65 let s = arg_str(args);
66 let has_letter = s.chars().any(|c| c.is_alphabetic());
67 b(has_letter
68 && s.chars()
69 .filter(|c| c.is_alphabetic())
70 .all(|c| c.is_lowercase()))
71}
72
73pub fn is_uppercase(args: &[StrykeValue]) -> StrykeValue {
74 let s = arg_str(args);
75 let has_letter = s.chars().any(|c| c.is_alphabetic());
76 b(has_letter
77 && s.chars()
78 .filter(|c| c.is_alphabetic())
79 .all(|c| c.is_uppercase()))
80}
81
82pub fn is_titlecase(args: &[StrykeValue]) -> StrykeValue {
83 let s = arg_str(args);
84 if s.is_empty() {
85 return b(false);
86 }
87 for word in s.split_whitespace() {
89 let mut chars = word.chars();
90 match chars.next() {
91 Some(c) if c.is_uppercase() => {}
92 _ => return b(false),
93 }
94 if !chars.all(|c| !c.is_alphabetic() || c.is_lowercase()) {
95 return b(false);
96 }
97 }
98 b(true)
99}
100
101pub fn is_palindrome_str(args: &[StrykeValue]) -> StrykeValue {
102 let s = arg_str(args);
103 let clean: Vec<char> = s
104 .chars()
105 .filter(|c| c.is_alphanumeric())
106 .flat_map(|c| c.to_lowercase())
107 .collect();
108 if clean.is_empty() {
109 return b(false);
110 }
111 let n = clean.len();
112 for i in 0..n / 2 {
113 if clean[i] != clean[n - 1 - i] {
114 return b(false);
115 }
116 }
117 b(true)
118}
119
120pub fn is_hex(args: &[StrykeValue]) -> StrykeValue {
125 let s = arg_str(args);
126 let cleaned = s.trim_start_matches("0x").trim_start_matches("0X");
127 b(!cleaned.is_empty() && cleaned.chars().all(|c| c.is_ascii_hexdigit()))
128}
129
130pub fn is_octal(args: &[StrykeValue]) -> StrykeValue {
131 let s = arg_str(args);
132 let cleaned = s.trim_start_matches("0o").trim_start_matches("0O");
133 b(!cleaned.is_empty() && cleaned.chars().all(|c| ('0'..='7').contains(&c)))
134}
135
136pub fn is_binary(args: &[StrykeValue]) -> StrykeValue {
137 let s = arg_str(args);
138 let cleaned = s.trim_start_matches("0b").trim_start_matches("0B");
139 b(!cleaned.is_empty() && cleaned.chars().all(|c| c == '0' || c == '1'))
140}
141
142pub fn is_base32(args: &[StrykeValue]) -> StrykeValue {
143 let s = arg_str(args);
144 let cleaned = s.trim_end_matches('=');
145 b(!cleaned.is_empty() && cleaned.chars().all(|c| matches!(c, 'A'..='Z' | '2'..='7')))
146}
147
148pub fn is_md5_hash(args: &[StrykeValue]) -> StrykeValue {
149 let s = arg_str(args);
150 b(s.len() == 32 && s.chars().all(|c| c.is_ascii_hexdigit()))
151}
152
153pub fn is_sha1_hash(args: &[StrykeValue]) -> StrykeValue {
154 let s = arg_str(args);
155 b(s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit()))
156}
157
158pub fn is_sha256_hash(args: &[StrykeValue]) -> StrykeValue {
159 let s = arg_str(args);
160 b(s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()))
161}
162
163pub fn is_ipv6(args: &[StrykeValue]) -> StrykeValue {
168 b(arg_str(args).parse::<std::net::Ipv6Addr>().is_ok())
169}
170
171pub fn is_cidr(args: &[StrykeValue]) -> StrykeValue {
172 let s = arg_str(args);
173 let s = s.trim();
174 let Some((addr, prefix)) = s.split_once('/') else {
175 return b(false);
176 };
177 let Ok(ip) = addr.parse::<std::net::IpAddr>() else {
178 return b(false);
179 };
180 let Ok(p) = prefix.parse::<u8>() else {
181 return b(false);
182 };
183 let max = match ip {
184 std::net::IpAddr::V4(_) => 32,
185 std::net::IpAddr::V6(_) => 128,
186 };
187 b(p <= max)
188}
189
190pub fn is_mac(args: &[StrykeValue]) -> StrykeValue {
191 let s = arg_str(args);
192 let cleaned: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
193 b(cleaned.len() == 12)
194}
195
196pub fn is_url_http(args: &[StrykeValue]) -> StrykeValue {
201 let s = arg_str(args);
202 let lower = s.trim().to_ascii_lowercase();
203 b(lower.starts_with("http://") && s.len() > 7)
204}
205
206pub fn is_url_https(args: &[StrykeValue]) -> StrykeValue {
207 let s = arg_str(args);
208 let lower = s.trim().to_ascii_lowercase();
209 b(lower.starts_with("https://") && s.len() > 8)
210}
211
212fn uuid_version_check(s: &str, expected: u8) -> bool {
214 if s.len() != 36 {
216 return false;
217 }
218 let bytes = s.as_bytes();
219 let dashes = [8, 13, 18, 23];
220 for (i, b) in bytes.iter().enumerate() {
221 if dashes.contains(&i) {
222 if *b != b'-' {
223 return false;
224 }
225 } else if !b.is_ascii_hexdigit() {
226 return false;
227 }
228 }
229 let version_char = bytes[14] as char;
231 version_char.to_digit(16) == Some(expected as u32)
232}
233
234pub fn is_uuid_v4(args: &[StrykeValue]) -> StrykeValue {
235 b(uuid_version_check(&arg_str(args), 4))
236}
237
238pub fn is_uuid_v7(args: &[StrykeValue]) -> StrykeValue {
239 b(uuid_version_check(&arg_str(args), 7))
240}
241
242pub fn is_jwt(args: &[StrykeValue]) -> StrykeValue {
243 let s = arg_str(args);
244 let parts: Vec<&str> = s.split('.').collect();
245 if parts.len() != 3 {
246 return b(false);
247 }
248 for p in &parts {
250 if p.is_empty() {
251 return b(false);
252 }
253 if !p
254 .chars()
255 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
256 {
257 return b(false);
258 }
259 }
260 b(true)
261}
262
263pub fn is_email_strict(args: &[StrykeValue]) -> StrykeValue {
264 let s = arg_str(args);
266 let s = s.trim();
267 if s.len() > 254 {
268 return b(false);
269 }
270 let Some(at_pos) = s.rfind('@') else {
271 return b(false);
272 };
273 let local = &s[..at_pos];
274 let domain = &s[at_pos + 1..];
275 if local.is_empty() || local.len() > 64 {
276 return b(false);
277 }
278 if domain.is_empty() || !domain.contains('.') {
279 return b(false);
280 }
281 if local.starts_with('.') || local.ends_with('.') || local.contains("..") {
283 return b(false);
284 }
285 for c in local.chars() {
286 if !matches!(c,
287 'a'..='z' | 'A'..='Z' | '0'..='9'
288 | '!' | '#' | '$' | '%' | '&' | '\''
289 | '*' | '+' | '-' | '/' | '=' | '?' | '^'
290 | '_' | '`' | '{' | '|' | '}' | '~' | '.'
291 ) {
292 return b(false);
293 }
294 }
295 for label in domain.split('.') {
297 if label.is_empty() || label.len() > 63 {
298 return b(false);
299 }
300 if label.starts_with('-') || label.ends_with('-') {
301 return b(false);
302 }
303 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
304 return b(false);
305 }
306 }
307 b(true)
308}
309
310fn luhn_valid(s: &str) -> bool {
316 let digits: Vec<u32> = s
317 .chars()
318 .filter(|c| c.is_ascii_digit())
319 .map(|c| c.to_digit(10).unwrap())
320 .collect();
321 if digits.len() < 2 {
322 return false;
323 }
324 let mut sum = 0u32;
325 for (i, d) in digits.iter().rev().enumerate() {
326 if i % 2 == 1 {
327 let doubled = d * 2;
328 sum += if doubled > 9 { doubled - 9 } else { doubled };
329 } else {
330 sum += d;
331 }
332 }
333 sum.is_multiple_of(10)
334}
335
336pub fn luhn_digit(args: &[StrykeValue]) -> StrykeValue {
338 let s = arg_str(args);
339 let digits: Vec<u32> = s
340 .chars()
341 .filter(|c| c.is_ascii_digit())
342 .map(|c| c.to_digit(10).unwrap())
343 .collect();
344 if digits.is_empty() {
345 return StrykeValue::UNDEF;
346 }
347 let mut sum = 0u32;
349 for (i, d) in digits.iter().rev().enumerate() {
350 if i % 2 == 0 {
351 let doubled = d * 2;
352 sum += if doubled > 9 { doubled - 9 } else { doubled };
353 } else {
354 sum += d;
355 }
356 }
357 let check = (10 - (sum % 10)) % 10;
358 StrykeValue::integer(check as i64)
359}
360
361pub fn is_imei(args: &[StrykeValue]) -> StrykeValue {
362 let s = arg_str(args);
363 let digits_only: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
364 b(digits_only.len() == 15 && luhn_valid(&digits_only))
365}
366
367pub fn is_imsi(args: &[StrykeValue]) -> StrykeValue {
368 let s = arg_str(args);
369 let digits_only: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
370 b(matches!(digits_only.len(), 14..=15) && digits_only.chars().all(|c| c.is_ascii_digit()))
371}
372
373pub fn is_vin(args: &[StrykeValue]) -> StrykeValue {
374 let s = arg_str(args).to_ascii_uppercase();
375 if s.len() != 17 {
376 return b(false);
377 }
378 if s.chars().any(|c| c == 'I' || c == 'O' || c == 'Q') {
380 return b(false);
381 }
382 let v = |c: char| -> Option<u32> {
384 match c {
385 '0'..='9' => Some(c.to_digit(10).unwrap()),
386 'A' | 'J' => Some(1),
387 'B' | 'K' | 'S' => Some(2),
388 'C' | 'L' | 'T' => Some(3),
389 'D' | 'M' | 'U' => Some(4),
390 'E' | 'N' | 'V' => Some(5),
391 'F' | 'W' => Some(6),
392 'G' | 'P' | 'X' => Some(7),
393 'H' | 'Y' => Some(8),
394 'R' | 'Z' => Some(9),
395 _ => None,
396 }
397 };
398 let weights: [u32; 17] = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
399 let mut sum = 0u32;
400 for (i, c) in s.chars().enumerate() {
401 let Some(vv) = v(c) else {
402 return b(false);
403 };
404 sum += vv * weights[i];
405 }
406 let check = sum % 11;
407 let expected = s.chars().nth(8).unwrap();
408 let check_char = if check == 10 {
409 'X'
410 } else {
411 std::char::from_digit(check, 10).unwrap()
412 };
413 b(expected == check_char)
414}
415
416pub fn vin_decode(args: &[StrykeValue]) -> StrykeValue {
419 use indexmap::IndexMap;
420 use parking_lot::RwLock;
421 use std::sync::Arc;
422 let s = arg_str(args).to_ascii_uppercase();
423 if s.len() != 17 {
424 return StrykeValue::UNDEF;
425 }
426 let wmi = &s[0..3];
427 let vds = &s[3..9];
428 let vis = &s[9..17];
429 let year_char = s.chars().nth(9).unwrap();
430 let plant = s.chars().nth(10).unwrap();
431 let year_letter_to_offset = |c: char| -> Option<u32> {
433 match c {
434 'A' => Some(10),
435 'B' => Some(11),
436 'C' => Some(12),
437 'D' => Some(13),
438 'E' => Some(14),
439 'F' => Some(15),
440 'G' => Some(16),
441 'H' => Some(17),
442 'J' => Some(18),
443 'K' => Some(19),
444 'L' => Some(20),
445 'M' => Some(21),
446 'N' => Some(22),
447 'P' => Some(23),
448 'R' => Some(24),
449 'S' => Some(25),
450 'T' => Some(26),
451 'V' => Some(27),
452 'W' => Some(28),
453 'X' => Some(29),
454 'Y' => Some(0),
455 '1' => Some(1),
456 '2' => Some(2),
457 '3' => Some(3),
458 '4' => Some(4),
459 '5' => Some(5),
460 '6' => Some(6),
461 '7' => Some(7),
462 '8' => Some(8),
463 '9' => Some(9),
464 _ => None,
465 }
466 };
467 let year = year_letter_to_offset(year_char).map(|o| 2000 + o);
468 let mut h: IndexMap<String, StrykeValue> = IndexMap::new();
469 h.insert("wmi".to_string(), StrykeValue::string(wmi.to_string()));
470 h.insert("vds".to_string(), StrykeValue::string(vds.to_string()));
471 h.insert("vis".to_string(), StrykeValue::string(vis.to_string()));
472 if let Some(y) = year {
473 h.insert("year".to_string(), StrykeValue::integer(y as i64));
474 }
475 h.insert("plant".to_string(), StrykeValue::string(plant.to_string()));
476 StrykeValue::hash_ref(Arc::new(RwLock::new(h)))
477}
478
479pub fn is_ean13(args: &[StrykeValue]) -> StrykeValue {
480 let s = arg_str(args);
481 let digits: Vec<u32> = s
482 .chars()
483 .filter(|c| c.is_ascii_digit())
484 .map(|c| c.to_digit(10).unwrap())
485 .collect();
486 if digits.len() != 13 {
487 return b(false);
488 }
489 let mut sum = 0u32;
490 for (i, d) in digits.iter().take(12).enumerate() {
491 sum += d * if i % 2 == 0 { 1 } else { 3 };
492 }
493 let check = (10 - (sum % 10)) % 10;
494 b(check == digits[12])
495}
496
497pub fn is_upc(args: &[StrykeValue]) -> StrykeValue {
498 let s = arg_str(args);
499 let digits: Vec<u32> = s
500 .chars()
501 .filter(|c| c.is_ascii_digit())
502 .map(|c| c.to_digit(10).unwrap())
503 .collect();
504 if digits.len() != 12 {
505 return b(false);
506 }
507 let mut sum = 0u32;
508 for (i, d) in digits.iter().take(11).enumerate() {
509 sum += d * if i % 2 == 0 { 3 } else { 1 };
510 }
511 let check = (10 - (sum % 10)) % 10;
512 b(check == digits[11])
513}
514
515pub fn is_isbn(args: &[StrykeValue]) -> StrykeValue {
516 let s = arg_str(args);
517 let cleaned: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
518 match cleaned.len() {
519 10 => b(isbn10_valid(&cleaned)),
520 13 => b(isbn13_valid(&cleaned)),
521 _ => b(false),
522 }
523}
524
525fn isbn10_valid(s: &str) -> bool {
526 if s.len() != 10 {
527 return false;
528 }
529 let mut sum = 0u32;
530 for (i, c) in s.chars().enumerate() {
531 let v = if i == 9 && c == 'X' {
532 10
533 } else if c.is_ascii_digit() {
534 c.to_digit(10).unwrap()
535 } else {
536 return false;
537 };
538 sum += v * (10 - i as u32);
539 }
540 sum.is_multiple_of(11)
541}
542
543fn isbn13_valid(s: &str) -> bool {
544 if s.len() != 13 {
545 return false;
546 }
547 let digits: Vec<u32> = match s
548 .chars()
549 .map(|c| c.to_digit(10).ok_or(()))
550 .collect::<Result<Vec<_>, _>>()
551 {
552 Ok(d) => d,
553 Err(_) => return false,
554 };
555 let mut sum = 0u32;
556 for (i, d) in digits.iter().take(12).enumerate() {
557 sum += d * if i % 2 == 0 { 1 } else { 3 };
558 }
559 let check = (10 - (sum % 10)) % 10;
560 check == digits[12]
561}
562
563pub fn isbn10_to_isbn13(args: &[StrykeValue]) -> StrykeValue {
564 let s = arg_str(args);
565 let cleaned: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
566 if cleaned.len() != 10 || !isbn10_valid(&cleaned) {
567 return StrykeValue::UNDEF;
568 }
569 let body: String = format!("978{}", &cleaned[..9]);
570 let digits: Vec<u32> = body.chars().map(|c| c.to_digit(10).unwrap()).collect();
571 let mut sum = 0u32;
572 for (i, d) in digits.iter().enumerate() {
573 sum += d * if i % 2 == 0 { 1 } else { 3 };
574 }
575 let check = (10 - (sum % 10)) % 10;
576 StrykeValue::string(format!("{}{}", body, check))
577}
578
579pub fn isbn13_to_isbn10(args: &[StrykeValue]) -> StrykeValue {
580 let s = arg_str(args);
581 let cleaned: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
582 if cleaned.len() != 13 || !isbn13_valid(&cleaned) {
583 return StrykeValue::UNDEF;
584 }
585 if !cleaned.starts_with("978") {
586 return StrykeValue::UNDEF;
588 }
589 let body = &cleaned[3..12];
590 let digits: Vec<u32> = body.chars().map(|c| c.to_digit(10).unwrap()).collect();
591 let mut sum = 0u32;
592 for (i, d) in digits.iter().enumerate() {
593 sum += d * (10 - i as u32);
594 }
595 let r = sum % 11;
596 let check = (11 - r) % 11;
597 let check_char = if check == 10 {
598 'X'
599 } else {
600 std::char::from_digit(check, 10).unwrap()
601 };
602 StrykeValue::string(format!("{}{}", body, check_char))
603}
604
605pub fn iban_format(args: &[StrykeValue]) -> StrykeValue {
611 let s = arg_str(args);
612 let cleaned: String = s
613 .chars()
614 .filter(|c| !c.is_whitespace())
615 .map(|c| c.to_ascii_uppercase())
616 .collect();
617 let mut out = String::new();
619 for (i, c) in cleaned.chars().enumerate() {
620 if i > 0 && i % 4 == 0 {
621 out.push(' ');
622 }
623 out.push(c);
624 }
625 StrykeValue::string(out)
626}
627
628pub fn iban_country(args: &[StrykeValue]) -> StrykeValue {
629 let s = arg_str(args);
630 let cleaned: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
631 if cleaned.len() < 2 {
632 return StrykeValue::UNDEF;
633 }
634 let cc: String = cleaned
635 .chars()
636 .take(2)
637 .collect::<String>()
638 .to_ascii_uppercase();
639 if cc.chars().all(|c| c.is_ascii_alphabetic()) {
640 StrykeValue::string(cc)
641 } else {
642 StrykeValue::UNDEF
643 }
644}
645
646pub fn is_bic(args: &[StrykeValue]) -> StrykeValue {
652 let s = arg_str(args).to_ascii_uppercase();
653 if s.len() != 8 && s.len() != 11 {
654 return b(false);
655 }
656 let chars: Vec<char> = s.chars().collect();
657 for &c in &chars[0..4] {
659 if !c.is_ascii_alphabetic() {
660 return b(false);
661 }
662 }
663 for &c in &chars[4..6] {
665 if !c.is_ascii_alphabetic() {
666 return b(false);
667 }
668 }
669 for &c in &chars[6..8] {
671 if !c.is_ascii_alphanumeric() {
672 return b(false);
673 }
674 }
675 if chars.len() == 11 {
676 for &c in &chars[8..11] {
678 if !c.is_ascii_alphanumeric() {
679 return b(false);
680 }
681 }
682 }
683 b(true)
684}
685
686pub fn is_swift(args: &[StrykeValue]) -> StrykeValue {
687 is_bic(args)
688}
689
690pub fn is_phone(args: &[StrykeValue]) -> StrykeValue {
695 let s = arg_str(args);
696 let digit_count = s.chars().filter(|c| c.is_ascii_digit()).count();
697 b((7..=15).contains(&digit_count)
699 && s.chars()
700 .all(|c| c.is_ascii_digit() || c.is_whitespace() || "+-().".contains(c)))
701}
702
703pub fn is_phone_e164(args: &[StrykeValue]) -> StrykeValue {
704 let s = arg_str(args);
705 let s = s.trim();
706 if !s.starts_with('+') {
707 return b(false);
708 }
709 let digits: String = s[1..].chars().filter(|c| c.is_ascii_digit()).collect();
710 b(digits.len() >= 8 && digits.len() <= 15 && s[1..].chars().all(|c| c.is_ascii_digit()))
711}
712
713pub fn is_zip_us(args: &[StrykeValue]) -> StrykeValue {
714 let s = arg_str(args).trim().to_string();
715 b(s.len() == 5 && s.chars().all(|c| c.is_ascii_digit()))
716}
717
718pub fn is_zip_plus4(args: &[StrykeValue]) -> StrykeValue {
719 let s = arg_str(args).trim().to_string();
720 if s.len() != 10 {
721 return b(false);
722 }
723 let bytes = s.as_bytes();
724 b(bytes[5] == b'-'
725 && s[0..5].chars().all(|c| c.is_ascii_digit())
726 && s[6..10].chars().all(|c| c.is_ascii_digit()))
727}
728
729pub fn is_postal_code(args: &[StrykeValue]) -> StrykeValue {
733 let code = arg_str(args).trim().to_ascii_uppercase();
734 let country = args
735 .get(1)
736 .map(|v| v.to_string().trim().to_ascii_uppercase())
737 .unwrap_or_else(|| "US".to_string());
738 let ok = match country.as_str() {
739 "US" => {
740 code.len() == 5 && code.chars().all(|c| c.is_ascii_digit())
741 || (code.len() == 10
742 && code.chars().nth(5) == Some('-')
743 && code[..5].chars().all(|c| c.is_ascii_digit())
744 && code[6..].chars().all(|c| c.is_ascii_digit()))
745 }
746 "CA" => {
747 let cleaned: String = code.chars().filter(|c| !c.is_whitespace()).collect();
749 cleaned.len() == 6
750 && cleaned.chars().enumerate().all(|(i, c)| {
751 if i % 2 == 0 {
752 c.is_ascii_alphabetic()
753 } else {
754 c.is_ascii_digit()
755 }
756 })
757 }
758 "UK" | "GB" => {
759 let cleaned: String = code.chars().filter(|c| !c.is_whitespace()).collect();
760 (5..=7).contains(&cleaned.len())
761 }
762 "DE" | "FR" | "IT" | "ES" => code.len() == 5 && code.chars().all(|c| c.is_ascii_digit()),
763 "JP" => {
764 let cleaned: String = code.chars().filter(|c| c.is_ascii_digit()).collect();
765 cleaned.len() == 7
766 }
767 "AU" | "BE" | "DK" | "NO" | "CH" | "AT" => {
768 code.len() == 4 && code.chars().all(|c| c.is_ascii_digit())
769 }
770 "NL" => {
771 let cleaned: String = code.chars().filter(|c| !c.is_whitespace()).collect();
772 cleaned.len() == 6
773 && cleaned[..4].chars().all(|c| c.is_ascii_digit())
774 && cleaned[4..].chars().all(|c| c.is_ascii_alphabetic())
775 }
776 "BR" => {
777 let cleaned: String = code.chars().filter(|c| c.is_ascii_digit()).collect();
778 cleaned.len() == 8
779 }
780 _ => {
781 let cleaned: String = code.chars().filter(|c| !c.is_whitespace()).collect();
782 (3..=10).contains(&cleaned.len()) && cleaned.chars().all(|c| c.is_ascii_alphanumeric())
783 }
784 };
785 b(ok)
786}
787
788pub fn is_ssn_us(args: &[StrykeValue]) -> StrykeValue {
789 let s = arg_str(args).trim().to_string();
790 if s.len() != 11 && s.len() != 9 {
791 return b(false);
792 }
793 let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
794 if digits.len() != 9 {
795 return b(false);
796 }
797 let area = &digits[0..3];
799 let group = &digits[3..5];
800 let serial = &digits[5..9];
801 if area == "000" || area == "666" || area.starts_with('9') {
802 return b(false);
803 }
804 if group == "00" || serial == "0000" {
805 return b(false);
806 }
807 if s.len() == 11 {
808 let bytes = s.as_bytes();
810 if bytes[3] != b'-' || bytes[6] != b'-' {
811 return b(false);
812 }
813 }
814 b(true)
815}
816
817fn parse_semver(s: &str) -> Option<(u64, u64, u64, String, String)> {
823 let s = s.trim();
824 let (core, build) = match s.split_once('+') {
825 Some((c, b)) => (c, b.to_string()),
826 None => (s, String::new()),
827 };
828 let (core, pre) = match core.split_once('-') {
829 Some((c, p)) => (c, p.to_string()),
830 None => (core, String::new()),
831 };
832 let parts: Vec<&str> = core.split('.').collect();
833 if parts.len() != 3 {
834 return None;
835 }
836 let major = parts[0].parse::<u64>().ok()?;
837 let minor = parts[1].parse::<u64>().ok()?;
838 let patch = parts[2].parse::<u64>().ok()?;
839 Some((major, minor, patch, pre, build))
840}
841
842pub fn semver_compare(args: &[StrykeValue]) -> StrykeValue {
843 let a = args.first().map(|v| v.to_string()).unwrap_or_default();
844 let bs = args.get(1).map(|v| v.to_string()).unwrap_or_default();
845 let Some((a_maj, a_min, a_pat, a_pre, _)) = parse_semver(&a) else {
846 return StrykeValue::UNDEF;
847 };
848 let Some((b_maj, b_min, b_pat, b_pre, _)) = parse_semver(&bs) else {
849 return StrykeValue::UNDEF;
850 };
851 use std::cmp::Ordering;
852 let ord = (a_maj, a_min, a_pat).cmp(&(b_maj, b_min, b_pat));
853 let ord = if ord != Ordering::Equal {
854 ord
855 } else {
856 match (a_pre.is_empty(), b_pre.is_empty()) {
858 (true, true) => Ordering::Equal,
859 (true, false) => Ordering::Greater,
860 (false, true) => Ordering::Less,
861 (false, false) => a_pre.cmp(&b_pre),
862 }
863 };
864 StrykeValue::integer(match ord {
865 Ordering::Less => -1,
866 Ordering::Equal => 0,
867 Ordering::Greater => 1,
868 })
869}
870
871pub fn semver_satisfies(args: &[StrykeValue]) -> StrykeValue {
875 let v = args.first().map(|v| v.to_string()).unwrap_or_default();
876 let range = args.get(1).map(|v| v.to_string()).unwrap_or_default();
877 let range = range.trim();
878 let (op, rhs) = if let Some(r) = range.strip_prefix(">=") {
879 (">=", r.trim())
880 } else if let Some(r) = range.strip_prefix("<=") {
881 ("<=", r.trim())
882 } else if let Some(r) = range.strip_prefix("!=") {
883 ("!=", r.trim())
884 } else if let Some(r) = range.strip_prefix('>') {
885 (">", r.trim())
886 } else if let Some(r) = range.strip_prefix('<') {
887 ("<", r.trim())
888 } else if let Some(r) = range.strip_prefix('=') {
889 ("=", r.trim())
890 } else {
891 ("=", range)
892 };
893 let cmp = semver_compare(&[StrykeValue::string(v), StrykeValue::string(rhs.to_string())]);
894 if cmp.is_undef() {
895 return StrykeValue::UNDEF;
896 }
897 let c = cmp.to_int();
898 let ok = match op {
899 "=" => c == 0,
900 "!=" => c != 0,
901 ">" => c > 0,
902 ">=" => c >= 0,
903 "<" => c < 0,
904 "<=" => c <= 0,
905 _ => false,
906 };
907 b(ok)
908}
909
910pub fn semver_increment_major(args: &[StrykeValue]) -> StrykeValue {
911 let Some((maj, _, _, _, _)) = parse_semver(&arg_str(args)) else {
912 return StrykeValue::UNDEF;
913 };
914 StrykeValue::string(format!("{}.0.0", maj + 1))
915}
916
917pub fn semver_increment_minor(args: &[StrykeValue]) -> StrykeValue {
918 let Some((maj, min, _, _, _)) = parse_semver(&arg_str(args)) else {
919 return StrykeValue::UNDEF;
920 };
921 StrykeValue::string(format!("{}.{}.0", maj, min + 1))
922}
923
924pub fn semver_increment_patch(args: &[StrykeValue]) -> StrykeValue {
925 let Some((maj, min, pat, _, _)) = parse_semver(&arg_str(args)) else {
926 return StrykeValue::UNDEF;
927 };
928 StrykeValue::string(format!("{}.{}.{}", maj, min, pat + 1))
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934
935 fn s(s: &str) -> StrykeValue {
936 StrykeValue::string(s.to_string())
937 }
938
939 #[test]
940 fn character_class_predicates() {
941 assert_eq!(is_alpha_only(&[s("hello")]).to_int(), 1);
942 assert_eq!(is_alpha_only(&[s("hi42")]).to_int(), 0);
943 assert_eq!(is_alphanumeric_only(&[s("abc123")]).to_int(), 1);
944 assert_eq!(is_alphanumeric_only(&[s("abc-123")]).to_int(), 0);
945 assert_eq!(is_numeric_only(&[s("12345")]).to_int(), 1);
946 assert_eq!(is_lowercase(&[s("hello")]).to_int(), 1);
947 assert_eq!(is_lowercase(&[s("HELLO")]).to_int(), 0);
948 assert_eq!(is_uppercase(&[s("HELLO")]).to_int(), 1);
949 assert_eq!(is_titlecase(&[s("Hello World")]).to_int(), 1);
950 assert_eq!(is_titlecase(&[s("hello world")]).to_int(), 0);
951 }
952
953 #[test]
954 fn palindrome_check() {
955 assert_eq!(is_palindrome_str(&[s("racecar")]).to_int(), 1);
956 assert_eq!(
957 is_palindrome_str(&[s("A man a plan a canal Panama")]).to_int(),
958 1
959 );
960 assert_eq!(is_palindrome_str(&[s("hello")]).to_int(), 0);
961 }
962
963 #[test]
964 fn hex_octal_binary() {
965 assert_eq!(is_hex(&[s("0xDEADBEEF")]).to_int(), 1);
966 assert_eq!(is_hex(&[s("xyz")]).to_int(), 0);
967 assert_eq!(is_octal(&[s("0o755")]).to_int(), 1);
968 assert_eq!(is_octal(&[s("999")]).to_int(), 0);
969 assert_eq!(is_binary(&[s("0b101010")]).to_int(), 1);
970 assert_eq!(is_binary(&[s("0b102")]).to_int(), 0);
971 }
972
973 #[test]
974 fn hash_lengths() {
975 assert_eq!(is_md5_hash(&[s(&"a".repeat(32))]).to_int(), 1);
976 assert_eq!(is_md5_hash(&[s(&"a".repeat(31))]).to_int(), 0);
977 assert_eq!(is_sha1_hash(&[s(&"f".repeat(40))]).to_int(), 1);
978 assert_eq!(is_sha256_hash(&[s(&"0".repeat(64))]).to_int(), 1);
979 }
980
981 #[test]
982 fn uuid_versions() {
983 let uuid_v4 = "550e8400-e29b-41d4-a716-446655440000";
985 assert_eq!(is_uuid_v4(&[s(uuid_v4)]).to_int(), 1);
986 let uuid_v7 = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
987 assert_eq!(is_uuid_v7(&[s(uuid_v7)]).to_int(), 1);
988 assert_eq!(is_uuid_v4(&[s(uuid_v7)]).to_int(), 0);
989 }
990
991 #[test]
992 fn jwt_basic() {
993 let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.SflKxwRJSMeKKF2QT4f";
995 assert_eq!(is_jwt(&[s(jwt)]).to_int(), 1);
996 assert_eq!(is_jwt(&[s("not.a.jwt.too.many.dots")]).to_int(), 0);
997 assert_eq!(is_jwt(&[s("only.two")]).to_int(), 0);
998 }
999
1000 #[test]
1001 fn email_strict_dot_atom() {
1002 assert_eq!(is_email_strict(&[s("a@b.co")]).to_int(), 1);
1003 assert_eq!(is_email_strict(&[s("alice+tag@example.com")]).to_int(), 1);
1004 assert_eq!(is_email_strict(&[s("no-at-symbol")]).to_int(), 0);
1005 assert_eq!(is_email_strict(&[s("..@bad.com")]).to_int(), 0);
1006 assert_eq!(is_email_strict(&[s("foo@bar")]).to_int(), 0); }
1008
1009 #[test]
1010 fn imei_luhn() {
1011 assert_eq!(is_imei(&[s("490154203237518")]).to_int(), 1);
1013 assert_eq!(is_imei(&[s("490154203237519")]).to_int(), 0);
1014 }
1015
1016 #[test]
1017 fn vin_valid() {
1018 assert_eq!(is_vin(&[s("1M8GDM9AXKP042788")]).to_int(), 1);
1020 }
1021
1022 #[test]
1023 fn isbn_round_trips() {
1024 assert_eq!(
1026 isbn10_to_isbn13(&[s("0306406152")]).to_string(),
1027 "9780306406157"
1028 );
1029 assert_eq!(
1030 isbn13_to_isbn10(&[s("9780306406157")]).to_string(),
1031 "0306406152"
1032 );
1033 }
1034
1035 #[test]
1036 fn ean13_upc() {
1037 assert_eq!(is_ean13(&[s("4006381333931")]).to_int(), 1);
1038 assert_eq!(is_upc(&[s("036000291452")]).to_int(), 1);
1039 }
1040
1041 #[test]
1042 fn zip_us_variants() {
1043 assert_eq!(is_zip_us(&[s("12345")]).to_int(), 1);
1044 assert_eq!(is_zip_us(&[s("1234")]).to_int(), 0);
1045 assert_eq!(is_zip_plus4(&[s("12345-6789")]).to_int(), 1);
1046 assert_eq!(is_zip_plus4(&[s("123456789")]).to_int(), 0);
1047 }
1048
1049 #[test]
1050 fn semver_ops() {
1051 assert_eq!(semver_compare(&[s("1.2.3"), s("1.2.4")]).to_int(), -1);
1052 assert_eq!(semver_compare(&[s("2.0.0"), s("1.999.999")]).to_int(), 1);
1053 assert_eq!(semver_compare(&[s("1.0.0-alpha"), s("1.0.0")]).to_int(), -1);
1054 assert_eq!(semver_satisfies(&[s("1.2.3"), s(">=1.0.0")]).to_int(), 1);
1055 assert_eq!(semver_satisfies(&[s("1.2.3"), s("<2.0.0")]).to_int(), 1);
1056 assert_eq!(semver_increment_major(&[s("1.2.3")]).to_string(), "2.0.0");
1057 assert_eq!(semver_increment_minor(&[s("1.2.3")]).to_string(), "1.3.0");
1058 assert_eq!(semver_increment_patch(&[s("1.2.3")]).to_string(), "1.2.4");
1059 }
1060
1061 #[test]
1062 fn phone_e164() {
1063 assert_eq!(is_phone_e164(&[s("+12025551234")]).to_int(), 1);
1064 assert_eq!(is_phone_e164(&[s("+44 20 7946 0958")]).to_int(), 0); assert_eq!(is_phone_e164(&[s("12025551234")]).to_int(), 0); }
1067
1068 #[test]
1069 fn bic_swift() {
1070 assert_eq!(is_bic(&[s("DEUTDEFF")]).to_int(), 1);
1071 assert_eq!(is_bic(&[s("DEUTDEFF500")]).to_int(), 1);
1072 assert_eq!(is_bic(&[s("DEUTDEFF50")]).to_int(), 0);
1073 }
1074}