Functional Weave
Code in Rust

validation.bic

Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 15 tests, run in TypeScript, Python and Rust.

What it does

Checks that a Business Identifier Code - the "SWIFT code" of a bank - is well formed, and splits it into its four parts. `DEUTDEFF500` is Deutsche Bank (`DEUT`), Germany (`DE`), Frankfurt (`FF`), branch `500`.

FORMAT, as the ISO 20022 payment messages validate it (the 2014 edition of ISO 9362, pattern `[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?`):

For example

  • validate_bic(DEUTDEFF) → valid true, normalised DEUTDEFF, institution DEUT, country DE, location FF, branch XXX, reason — an 8-character BIC is the primary office, branch XXX
  • validate_bic(DEUTDEFF500) → valid true, normalised DEUTDEFF500, institution DEUT, country DE, location FF, branch 500, reason — an 11-character BIC with a branch
  • validate_bic(NEDSZAJJXXX) → valid true, normalised NEDSZAJJXXX, institution NEDS, country ZA, location JJ, branch XXX, reason — the explicit primary office keeps its 11 characters

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn validate_bic(value: &str) -> BicCheck
valuestringa BIC in any case, optionally spaced
returnsBicCheck

The type it declares, generated into your project

/// The result of checking a BIC. Every string is null when valid is false; reason is null when it is true.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BicCheck {
    pub valid: bool,
    /// upper case, no spaces, 8 or 11 characters as given
    pub normalised: Option<String>,
    /// the four-character business party prefix, e.g. DEUT
    pub institution: Option<String>,
    /// the two-letter country code, e.g. DE
    pub country: Option<String>,
    /// the two-character location code, e.g. FF
    pub location: Option<String>,
    /// the three-character branch code; XXX (the primary office) for an 8-character BIC
    pub branch: Option<String>,
    /// empty, bad-character, bad-length or bad-country
    pub reason: Option<String>,
}

Your code names it in one line, in the file that uses it

fune!(validation.bic@^1);  // then call validate_bic(…)
impl/rust.rs · 73 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

fn invalid(reason: &str) -> BicCheck {
    BicCheck {
        valid: false,
        normalised: None,
        institution: None,
        country: None,
        location: None,
        branch: None,
        reason: Some(reason.to_string()),
    }
}

/// Check the format of a BIC and split it into institution, country, location
/// and branch.
///
/// Well formed is not registered: only SWIFT's directory knows whether the
/// code is assigned. Validators answer rather than panic.
pub fn validate_bic(value: &str) -> BicCheck {
    let bic: Vec<char> = value
        .chars()
        .filter(|ch| *ch != ' ')
        .map(|ch| ch.to_ascii_uppercase())
        .collect();
    if bic.is_empty() {
        return invalid("empty");
    }
    if bic.iter().any(|ch| !(ch.is_ascii_uppercase() || ch.is_ascii_digit())) {
        return invalid("bad-character");
    }
    if bic.len() != 8 && bic.len() != 11 {
        return invalid("bad-length");
    }
    if !bic[4].is_ascii_uppercase() || !bic[5].is_ascii_uppercase() {
        return invalid("bad-country");
    }

    let part = |from: usize, to: usize| -> Option<String> { Some(bic[from..to].iter().collect()) };
    BicCheck {
        valid: true,
        normalised: Some(bic.iter().collect()),
        institution: part(0, 4),
        country: part(4, 6),
        location: part(6, 8),
        // An 8-character BIC is the primary office, which the 11-character form spells XXX.
        branch: if bic.len() == 11 { part(8, 11) } else { Some("XXX".to_string()) },
        reason: None,
    }
}

/// Object keys are camelCase to match the shared vectors.
pub fn bic_check_to_value(result: &BicCheck) -> Value {
    let text = |field: &Option<String>| match field {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    Value::obj(vec![
        ("valid", Value::Bool(result.valid)),
        ("normalised", text(&result.normalised)),
        ("institution", text(&result.institution)),
        ("country", text(&result.country)),
        ("location", text(&result.location)),
        ("branch", text(&result.branch)),
        ("reason", text(&result.reason)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A non-string argument arrives here as an empty string, which is exactly
    // the answer TypeScript and Python give for a non-string: empty.
    bic_check_to_value(&validate_bic(args[0].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add validation.bic
Download for Rust validation.bic-1.0.0-rust.fune · 11,592 bytes sha256 883c0ed7ae9e67a80ecf57c9c2d2eeef5997cb7ceb3fbb7a7a8f7136fbcebb83

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./validation.bic-1.0.0-rust.fune, or fetch it from a terminal with fune pull validation.bic@1.0.0:rust.

The whole function, every language, is one file too: validation.bic-1.0.0.fune, 14,777 bytes, sha256 2a573b0bd05ab318b74e3aea6ddc5e1de63a1a838cce8211f58259a5ee40f76b. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before validation.bic

after — your function gets the result and the arguments, and returns the final result.

// fune: after validation.bic

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show validation.bic --steps.

// fune: step validation.bic after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
an 8-character BIC is the primary office, branch XXX DEUTDEFF → valid true, normalised DEUTDEFF, institution DEUT, country DE, location FF, branch XXX, reason —
an 11-character BIC with a branch DEUTDEFF500 → valid true, normalised DEUTDEFF500, institution DEUT, country DE, location FF, branch 500, reason —
the explicit primary office keeps its 11 characters NEDSZAJJXXX → valid true, normalised NEDSZAJJXXX, institution NEDS, country ZA, location JJ, branch XXX, reason —
a UK BIC with a digit in the location NWBKGB2L → valid true, normalised NWBKGB2L, institution NWBK, country GB, location 2L, branch XXX, reason —
lower case and spaces are normalised nwbk gb 2l → valid true, normalised NWBKGB2L, institution NWBK, country GB, location 2L, branch XXX, reason —
a lettered branch code DSBACNBXSHA → valid true, normalised DSBACNBXSHA, institution DSBA, country CN, location BX, branch SHA, reason —
digits in the institution prefix are allowed since ISO 9362:2014 1234GB2L → valid true, normalised 1234GB2L, institution 1234, country GB, location 2L, branch XXX, reason —
a digit in the country code is refused NWBK9B2L → valid false, normalised —, institution —, country —, location —, branch —, reason bad-country
seven characters is too short NWBKGB2 → valid false, normalised —, institution —, country —, location —, branch —, reason bad-length
nine characters is neither 8 nor 11 NWBKGB2L1 → valid false, normalised —, institution —, country —, location —, branch —, reason bad-length
Show the other 5 tests
CaseArgumentsExpected
twelve characters, a SWIFTNet terminal address, is not a BIC DEUTDEFFA500 → valid false, normalised —, institution —, country —, location —, branch —, reason bad-length
hyphens are not accepted separators NWBK-GB-2L → valid false, normalised —, institution —, country —, location —, branch —, reason bad-character
an accented letter is a bad character DEUTDÉFF → valid false, normalised —, institution —, country —, location —, branch —, reason bad-character
the empty string → valid false, normalised —, institution —, country —, location —, branch —, reason empty
a non-string is empty, not an exception 12,345,678 → valid false, normalised —, institution —, country —, location —, branch —, reason empty

More from the author

- 4 letters or digits: the business party prefix (institution). Every BIC issued so far uses letters here, but ISO 9362:2014 allows digits, and rejecting them would make this stricter than the payment messages it guards. - 2 letters: the country, an ISO 3166-1 alpha-2 code (plus `XK`, which SWIFT assigned to Kosovo). - 2 letters or digits: the location. A `0` in the second place conventionally marks a test BIC; this capability does not flag it, and it is still a well-formed code. - 3 letters or digits, optional: the branch. An 8-character BIC means the primary office and is the same as the 11-character form ending `XXX`, so `branch` is `XXX` for both. `normalised` keeps the length the caller gave, because some systems require one form or the other.

A WELL-FORMED BIC IS NOT A REGISTERED ONE. Only SWIFT's BIC directory can say that the code is assigned and still active. The country letters are checked to be letters, not looked up in the ISO 3166 list, which would need data and releases of its own; pair this with a country check if that matters.

ACCEPTED INPUT: letters in either case (folded to upper case) and digits, with ASCII spaces ignored anywhere ("DEUT DE FF"). Hyphens and other punctuation are refused.

| reason | meaning | |-----------------|------------------------------------------------------| | `empty` | nothing but spaces, or not a string at all | | `bad-character` | anything other than A-Z, a-z, 0-9 and spaces | | `bad-length` | not 8 or 11 characters | | `bad-country` | characters 5 and 6 are not both letters |

The checks run in that order; the first failure is the reason. Validators answer rather than throw.

Not enforced: SWIFT's operational conventions for particular characters in the location and branch codes, which are directory policy rather than format and are not in the ISO 20022 pattern.

Sources: ISO 9362:2014 / 2022 "Banking - Banking telecommunication messages - Business identifier code (BIC)"; the ISO 20022 BICFIDec2014Identifier and AnyBICDec2014Identifier patterns (https://www.iso20022.org); SWIFT, "Business Identifier Code (BIC)" (https://www.swift.com/standards/data-standards/bic-business-identifier-code).

Files

PathBytes
README.md2,694
impl/python.py1,468
impl/rust.rs2,426
impl/typescript.ts1,529
vectors.json3,393