Card numbers
Payment card numbers checked before they reach a payment provider: Luhn check digit, brand and length, expiry, and the number formatted or masked for receipts.
Version 1.0.0 · 5 functions · 0 crates
Functions
What use cards.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use card_check, card_valid from cards (Quillo 0.6).
card_check(number: str) → anyWhat a card number is: { valid, brand, luhn, length_ok, last4, cvc_length }. brand is "visa", "mastercard", "amex", "discover", "jcb", "diners", "unionpay", "maestro" or null; valid means the check digit and the length both fit that brand. Spaces and dashes are ignored. It does not say the card exists or has money: only the bank knows.
card_valid(number: str) → boolWhether number is a valid card number (check digit, length and a known brand).
card_mask(number: str) → stror an errorThe number masked for a receipt or a screen, in groups as printed on the card: "•••• •••• •••• 4242" (Amex: "•••• •••••• •1005"). An error when it is not digits.
card_format(number: str) → stror an errorThe number in groups as printed on the card, for a form field as it is typed: "4242424242424242" is "4242 4242 4242 4242", Amex in 4-6-5.
card_expiry_ok(month: int, year: int) → boolWhether a card's expiry month and year (two or four digits) are this month or later, by the server's clock in UTC.
Get it
$ quillo install cards # cards.qwer into this folder$ quillo install cards --example # and its example, cards.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/cards.qwer$ curl -O https://quillolang.org/plugins/files/cards.qweThen, in your program, all of it or only what you call:
use cards.qweruse card_check, card_valid from cards // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A checkout form's card field checked before the payment provider is called: brand, check digit, expiry, and a masked number for the receipt.//// quillo run cards.qwe then open http://localhost:8080/check?number=4242%204242%204242%204242use "cards.qwer"route GET /check ? { number: str } = { card: card_check(number), shown: card_mask(number) ?? null, typed: card_format(number) ?? null }route GET /test-cards = ["4242424242424242", "5555 5555 5555 4444", "3782 822463 10005", "6011111111111117", "4242424242424241"] | map((n) => { number: n, brand: card_check(n).brand, valid: card_valid(n), receipt: card_mask(n)? })route GET /expiry = { old: card_expiry_ok(1, 20), future: card_expiry_ok(12, 2035), bad_month: card_expiry_ok(13, 2030) }$ quillo run cards.qwe$ curl 'localhost:8080/check?number=4242%204242%204242%204242'$ curl 'localhost:8080/test-cards'$ curl 'localhost:8080/expiry'Source
//! Payment card numbers checked before they reach a payment provider: the Luhn check digit, the brand (Visa, Mastercard, Amex...), the right length, and the number masked for receipts.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};fn digits(number: &str) -> Option<String> { let d: String = number.chars().filter(|c| !c.is_whitespace() && *c != '-').collect(); if d.is_empty() || d.len() > 25 || !d.chars().all(|c| c.is_ascii_digit()) { None } else { Some(d) }}fn luhn_ok(d: &str) -> bool { let sum: u32 = d.chars().rev().enumerate().map(|(i, c)| { let n = c.to_digit(10).unwrap_or(0); if i % 2 == 1 { let x = n * 2; if x > 9 { x - 9 } else { x } } else { n } }).sum(); d.len() >= 2 && sum % 10 == 0}/// (brand, lengths, CVC length) by the number's first digits (its IIN range).fn brand(d: &str) -> Option<(&'static str, &'static [usize], usize)> { let p = |n: usize| d.get(..n).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0); let r = |n: usize, lo: u32, hi: u32| (lo..=hi).contains(&p(n)); Some(if r(2, 34, 34) || r(2, 37, 37) { ("amex", &[15], 4) } else if r(4, 2221, 2720) || r(2, 51, 55) { ("mastercard", &[16], 3) } else if r(4, 6011, 6011) || r(2, 65, 65) || r(3, 644, 649) || r(6, 622126, 622925) { ("discover", &[16, 17, 18, 19], 3) } else if r(4, 3528, 3589) { ("jcb", &[16, 17, 18, 19], 3) } else if r(2, 36, 36) || r(2, 38, 39) || r(3, 300, 305) { ("diners", &[14, 15, 16, 17, 18, 19], 3) } else if r(2, 62, 62) || r(2, 81, 81) { ("unionpay", &[16, 17, 18, 19], 3) } else if r(2, 50, 50) || r(2, 56, 58) || r(1, 6, 6) { ("maestro", &[12, 13, 14, 15, 16, 17, 18, 19], 3) } else if r(1, 4, 4) { ("visa", &[13, 16, 19], 3) } else { return None; })}/// What a card number is: `{ valid, brand, luhn, length_ok, last4, cvc_length }`. `brand` is "visa", "mastercard", "amex", "discover", "jcb", "diners", "unionpay", "maestro" or null; `valid` means the check digit and the length both fit that brand. Spaces and dashes are ignored. It does not say the card exists or has money: only the bank knows.pub fn card_check(number: &str) -> Value { let Some(d) = digits(number) else { return json!({ "valid": false, "brand": null, "luhn": false, "length_ok": false, "last4": null, "cvc_length": null }); }; let b = brand(&d); let luhn = luhn_ok(&d); let length_ok = b.map_or(d.len() >= 12 && d.len() <= 19, |(_, l, _)| l.contains(&d.len())); json!({ "valid": b.is_some() && luhn && length_ok, "brand": b.map(|x| x.0), "luhn": luhn, "length_ok": length_ok, "last4": d.get(d.len().saturating_sub(4)..), "cvc_length": b.map(|x| x.2), })}/// Whether `number` is a valid card number (check digit, length and a known brand).pub fn card_valid(number: &str) -> bool { card_check(number)["valid"].as_bool().unwrap_or(false)}/// The number masked for a receipt or a screen, in groups as printed on the card: "•••• •••• •••• 4242" (Amex: "•••• •••••• •1005"). An error when it is not digits.pub fn card_mask(number: &str) -> Result<String, String> { let d = digits(number).ok_or("a card number is digits, spaces and dashes")?; let keep = d.len().saturating_sub(4); let masked: Vec<char> = d.chars().enumerate().map(|(i, c)| if i < keep { '•' } else { c }).collect(); let groups: &[usize] = if brand(&d).map(|b| b.0) == Some("amex") { &[4, 6, 5] } else { &[4, 4, 4, 4, 4] }; let mut out = String::new(); let mut i = 0; for &g in groups { if i >= masked.len() { break; } if !out.is_empty() { out.push(' '); } out.extend(&masked[i..(i + g).min(masked.len())]); i += g; } if i < masked.len() { out.push(' '); out.extend(&masked[i..]); } Ok(out)}/// The number in groups as printed on the card, for a form field as it is typed: "4242424242424242" is "4242 4242 4242 4242", Amex in 4-6-5.pub fn card_format(number: &str) -> Result<String, String> { let d = digits(number).ok_or("a card number is digits, spaces and dashes")?; let groups: &[usize] = if brand(&d).map(|b| b.0) == Some("amex") { &[4, 6, 5] } else { &[4, 4, 4, 4, 3] }; let mut out = Vec::new(); let mut i = 0; for &g in groups { if i >= d.len() { break; } out.push(&d[i..(i + g).min(d.len())]); i += g; } if i < d.len() { out.push(&d[i..]); } Ok(out.join(" "))}/// Whether a card's expiry month and year (two or four digits) are this month or later, by the server's clock in UTC.pub fn card_expiry_ok(month: i64, year: i64) -> bool { if !(1..=12).contains(&month) { return false; } let year = if year < 100 { 2000 + year } else { year }; let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64); // Days since 1970 to a civil year and month (Howard Hinnant's algorithm). let z = secs / 86400 + 719468; let era = z.div_euclid(146097); let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 }; (year, month) >= (y, m)}Related
currencies.qwer · 1.0.0ISO 4217 currencies: names, symbols, minor units and the countries using them, prices formatted, and amounts turned into the cents payment APIs take.6 views · 7 downloadsIBAN and BIC iban.qwer · 1.0.0IBANs checked (country, length, check digits) and printed as banks print them, and BICs checked.9 views · 8 downloadsNational ID numbers national_ids.qwer · 1.0.0National identity numbers of 29 countries checked by their own rules (US SSN, UK NINO, CPF, Aadhaar, PH TIN, DNI and NIE, codice fiscale, PESEL...), with the birth date and sex read out where the number holds them.7 views · 7 downloadsPostal codes postal_codes.qwer · 1.0.0Postal codes checked for 45 countries and written the post office's way: SW1A 1AA, 1012 JS, K1A 0B1, 100-0001, 20500-0001.6 views · 7 downloadsUS routing numbers aba_routing.qwer · 1.0.0US bank routing numbers checked by their check digit and read for the Federal Reserve district and kind of institution, a check's MICR line read into routing, account and check number, and Canadian transit numbers.7 views · 6 downloadsVAT numbers vat.qwer · 1.0.0EU VAT numbers checked offline by format and check digits for all 27 countries, Northern Ireland, the UK, Switzerland and Norway, to catch typos before an invoice goes out.8 views · 8 downloadsThread 0 posts
On Card numbers: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.