├── Cargo.toml ├── .editorconfig ├── .github └── workflows │ ├── rust.yml │ └── publish.yml ├── .gitignore ├── tridgen ├── src │ └── main.rs └── Cargo.toml ├── LICENSE ├── trid ├── Cargo.toml └── src │ ├── tests.rs │ └── lib.rs ├── .vscode ├── tasks.json └── launch.json ├── CHANGES.md └── README.md /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["trid", "tridgen"] 3 | resolver = "3" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | indent_size = 4 4 | indent_style = space 5 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | - name: Run tests 21 | run: cargo test --verbose -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: ["release"] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Test 16 | run: cargo test --verbose 17 | - name: Publish 18 | run: cargo publish --verbose --token ${CARGO_REGISTRY_TOKEN} 19 | env: 20 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 21 | -------------------------------------------------------------------------------- /tridgen/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rand::Rng; 3 | use trid::TurkishId; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(author, version, about)] 7 | struct Args { 8 | #[arg(default_value_t = 1)] 9 | /// Number of ID numbers to be generated. 10 | count: u32, 11 | } 12 | 13 | fn main() { 14 | let args = Args::parse(); 15 | let count = args.count; 16 | let mut rng = rand::rng(); 17 | for _ in 0..count { 18 | let seq: u32 = rng.random_range(TurkishId::SEQ_RANGE); 19 | println!("{}", TurkishId::from_seq(seq).unwrap()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Sedat Kapanoglu 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /tridgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "tridgen" 4 | version = "0.2.1" 5 | description = "Turkish citizenship ID generator" 6 | readme = "../README.md" 7 | homepage = "https://github.com/ssg/trid" 8 | repository = "https://github.com/ssg/trid" 9 | authors = ["Sedat Kapanoglu "] 10 | license = "Apache-2.0" 11 | keywords = ["turkish", "turkey", "id", "number", "generator"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | trid = { version = "6.0.0", path = "../trid" } 16 | rand = "0.9.2" 17 | clap = { version = "4.3.2", features = ["derive"] } 18 | 19 | [[bin]] 20 | name = "tridgen" 21 | -------------------------------------------------------------------------------- /trid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "trid" 4 | version = "6.0.0" 5 | description = "Turkish citizenship ID number type" 6 | readme = "../README.md" 7 | homepage = "https://github.com/ssg/trid" 8 | repository = "https://github.com/ssg/trid" 9 | authors = ["Sedat Kapanoglu "] 10 | license = "Apache-2.0" 11 | keywords = ["turkish", "turkey", "id", "number"] 12 | categories = ["parser-implementations"] 13 | 14 | [lib] 15 | name = "trid" 16 | 17 | [lints.clippy] 18 | indexing_slicing = "deny" 19 | fallible_impl_from = "deny" 20 | wildcard_enum_match_arm = "deny" 21 | unneeded_field_pattern = "deny" 22 | fn_params_excessive_bools = "deny" 23 | must_use_candidate = "deny" -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "process", 6 | "command": "cargo", 7 | "args": [ 8 | "build", 9 | ], 10 | "problemMatcher": [ 11 | "$rustc" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | }, 17 | "label": "Rust: cargo build" 18 | }, 19 | { 20 | "type": "process", 21 | "command": "cargo", 22 | "args": [ 23 | "test", 24 | ], 25 | "problemMatcher": [ 26 | "$rustc" 27 | ], 28 | "group": { 29 | "kind": "test", 30 | "isDefault": true 31 | }, 32 | "label": "Rust: cargo test" 33 | }, 34 | { 35 | "type": "process", 36 | "command": "cargo", 37 | "args": [ 38 | "clippy", 39 | ], 40 | "problemMatcher": [ 41 | "$rustc" 42 | ], 43 | "group": "build", 44 | "label": "Rust: cargo clippy" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Run 'tridgen'", 11 | "cargo": { 12 | "args": ["run", "--package=tridgen"] 13 | }, 14 | "args": [], 15 | "cwd": "${workspaceFolder}" 16 | }, 17 | { 18 | "type": "lldb", 19 | "request": "launch", 20 | "name": "Debug unit tests in library 'trid'", 21 | "cargo": { 22 | "args": ["test", "--no-run", "--lib", "--package=trid"], 23 | "filter": { 24 | "name": "trid", 25 | "kind": "lib" 26 | } 27 | }, 28 | "args": [], 29 | "cwd": "${workspaceFolder}" 30 | }, 31 | { 32 | "type": "lldb", 33 | "request": "launch", 34 | "name": "Debug integration test 'is_valid'", 35 | "cargo": { 36 | "args": ["test", "--no-run", "--test=is_valid", "--package=trid"], 37 | "filter": { 38 | "name": "is_valid", 39 | "kind": "test" 40 | } 41 | }, 42 | "args": [], 43 | "cwd": "${workspaceFolder}" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 6.0.0 4 | 5 | ### Breaking changes 6 | 7 | - `Error::InvalidDigit` is now `Error::InvalidCharacter(c)` which contains 8 | the character that's invalid. 9 | 10 | - Invalid checksum errors are now split into `Error::InvalidFinalChecksum` 11 | and `Error::InvalidInitialChecksum`. 12 | 13 | - `is_valid` is now marked with `#[must_use]`. 14 | 15 | - The contents of TurkishId are now completely opaque to be able to keep the 16 | possibility to change the internal storage structure in the future. 17 | To see its contents, use `to_string()` or a formatter. 18 | 19 | ## 5.0.0 20 | 21 | ### Breaking changes 22 | 23 | - `TurkishIdError` is renamed to `Error` to conform to Rust semantics. Perhaps, 24 | I shouldn't have been so hesitant to make this package 1.0.0, huh :) 25 | 26 | - Added `from_seq()` method that can generate a valid TurkishId from a given 27 | sequence number. 28 | 29 | ## 4.0.0 30 | 31 | ### Breaking changes 32 | 33 | - Removed `TryFrom` impls for `&Bytes` and `&[u8]` types. The only conversion 34 | is possible from `&str` now. Changed the validation to use `&str` instead of 35 | `&[u8]`. The main reason I made this change is that it makes no sense to 36 | convert from a ASCII-encoded `&[u8]` not have it as `&str`. The conversion 37 | can alerady be done with `from_utf8()`, no need to repeat it there. 38 | 39 | - Removed `Display` impl from `TurkishIdError` completely as deriving `Debug` already does it. 40 | This might mean that any code that relies on the fmt output of `TurkishIdError` might break. 41 | 42 | ## 3.1.1 43 | 44 | ### Fixes 45 | 46 | - Fixed build break caused by inline source comments. 47 | 48 | ## 3.1.0 49 | 50 | ### Improvements 51 | 52 | - Avoid panic in `Display` impl completely. The code is completely panic-free now. 53 | 54 | ## 3.0.0 55 | 56 | ### New features 57 | 58 | - Now works without std (`no_std`-enabled), so feel free to use this in your favorite microcontroller :) 59 | 60 | ### Breaking changes 61 | 62 | - Due to `no_std`, `Error` trait isn't supported by `TurkishIdError` anymore 63 | 64 | ## 2.0.0 65 | 66 | ### Breaking changes 67 | 68 | - Implements `TryFrom` instead of `From` 69 | 70 | ### Improvements 71 | 72 | - Faster validation 73 | 74 | ## 1.0.0 75 | 76 | First stable release 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/trid)](https://crates.io/crates/trid) 2 | [![Build & Test](https://github.com/ssg/trid/actions/workflows/rust.yml/badge.svg)](https://github.com/ssg/trid/actions/workflows/rust.yml) 3 | 4 | # trid - Turkish Citizenship ID Number crate 5 | 6 | This is my first ever written Rust code derived from my own [TurkishId](https://github.com/ssg/TurkishId) 7 | package for .NET. I'm trying to use existing code as an excuse to learn about Rust. Despite constant tackling with error messages, Rust has been extremely impressive so far. (How cool are doc-tests!?) 8 | 9 | # Usage 10 | 11 | ## parse 12 | 13 | The crate provides `TurkishId` type that represents a valid Turkish ID number. 14 | It can be instantiated from a string using the `parse()` method of `str` type. 15 | `TurkishId` type guarantess that it never contains an invalid Turkish ID number, 16 | so there's no need to validate a `TurkishId` once parsed. It can always be 17 | passed around safely. 18 | 19 | ## is_valid 20 | 21 | You can just verify whether a string contains a valid Turkish ID or not 22 | by calling `is_valid(value: &str)` function. 23 | 24 | ## from_seq 25 | 26 | If you want to generate a Turkish ID from scratch, you can use 27 | `from_seq(seq: u32)` function which adds the checksum fields to the given 28 | sequence number. 29 | 30 | # Internals 31 | 32 | The type occupies 11 bytes in memory and kept as ASCII representation of 33 | the number in order to keep string conversions fast. The other 34 | alternative would be to have it in `u64` which would save 3 bytes but 35 | complicate the string conversions. 36 | 37 | # Examples 38 | 39 | Validate a Turkish citizenship ID number: 40 | 41 | ```rust 42 | fn main() { 43 | if trid::is_valid("12345678901") { 44 | // yayyy! 45 | } 46 | } 47 | ``` 48 | 49 | Try parsing a string into `TurkishId`: 50 | 51 | ```rust 52 | use trid::TurkishId; 53 | 54 | fn main() { 55 | let id : TurkishId = "12345678901".parse()?; 56 | } 57 | ``` 58 | 59 | Generate infinite number of random but valid Turkish IDs: 60 | 61 | ```rust 62 | use rand::Rng; 63 | use trid::TurkishId; 64 | 65 | fn main() { 66 | let mut rng = rand::thread_rng(); 67 | loop { 68 | let seq: u32 = rng.gen_range(100_000_000..1_000_000_000); 69 | println!("{}", TurkishId::from_seq(seq).unwrap()); 70 | } 71 | } 72 | ``` 73 | 74 | # License 75 | 76 | Apache License Version 2.0, see LICENSE file for details. 77 | -------------------------------------------------------------------------------- /trid/src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::collections::HashSet; 3 | 4 | const VALID_NUMBERS: &[&str] = &[ 5 | "19191919190", 6 | "76558242278", 7 | "80476431508", 8 | "76735508630", 9 | "90794350894", 10 | "43473624496", 11 | "10000000146", 12 | "56673392584", 13 | "29260807600", 14 | "93212606504", 15 | "35201408508", 16 | "64404737702", 17 | "10000000146", 18 | ]; 19 | 20 | const INVALID_NUMBERS: &[(&str, Error)] = &[ 21 | ("04948892948", Error::FirstDigitIsZero), // first digit zero 22 | ("14948892946", Error::InvalidFinalChecksum), // last checksum INVALID 23 | ("14948892937", Error::InvalidInitialChecksum), // first checksum INVALID 24 | // non numeric chars 25 | ("A4948892948", Error::InvalidCharacter('A')), 26 | ("7B558242278", Error::InvalidCharacter('B')), 27 | ("80C76431508", Error::InvalidCharacter('C')), 28 | ("767D5508630", Error::InvalidCharacter('D')), 29 | ("9079E350894", Error::InvalidCharacter('E')), 30 | ("43473F24496", Error::InvalidCharacter('F')), 31 | ("566733G2584", Error::InvalidCharacter('G')), 32 | ("2926080H600", Error::InvalidCharacter('H')), 33 | ("93212606I04", Error::InvalidCharacter('I')), 34 | ("352014085J8", Error::InvalidCharacter('J')), 35 | ("3520140853K", Error::InvalidCharacter('K')), 36 | // spaces 37 | (" 7655824227", Error::InvalidCharacter(' ')), 38 | ("5582422781 ", Error::InvalidCharacter(' ')), 39 | // uneven length 40 | ("", Error::InvalidLength), 41 | ("7", Error::InvalidLength), 42 | ("76", Error::InvalidLength), 43 | ("76558", Error::InvalidLength), 44 | ("765582", Error::InvalidLength), 45 | ("7655824", Error::InvalidLength), 46 | ("76558242", Error::InvalidLength), 47 | ("765582422", Error::InvalidLength), 48 | ("7655824227", Error::InvalidLength), 49 | ("765582422781", Error::InvalidLength), 50 | ]; 51 | 52 | const OUT_OF_RANGE_SEQUENCES: &[u32] = &[0, 99_999_999, 1_000_000_001, u32::MAX]; 53 | 54 | #[test] 55 | fn is_valid_validnumbers_returns_true() { 56 | for number in VALID_NUMBERS { 57 | assert!(is_valid(number)); 58 | } 59 | } 60 | 61 | #[test] 62 | fn is_valid_invalidnumbers_returns_false() { 63 | for (number, _) in INVALID_NUMBERS { 64 | assert!(!is_valid(number)); 65 | } 66 | } 67 | 68 | #[test] 69 | fn parse_invalidnumbers_returns_correct_error() { 70 | for (number, error) in INVALID_NUMBERS { 71 | assert_eq!(*error, number.parse::().err().unwrap()); 72 | } 73 | } 74 | 75 | #[test] 76 | fn hashset_compatible() { 77 | let mut set = HashSet::new(); 78 | let num = VALID_NUMBERS[0].parse::().unwrap(); 79 | set.insert(num); 80 | let num2 = VALID_NUMBERS[0].parse::().unwrap(); 81 | set.insert(num2); 82 | assert_eq!(num2, num); 83 | assert_eq!(1, set.len()); 84 | } 85 | 86 | #[test] 87 | fn display_returnsthesamerepresentation() { 88 | for number in VALID_NUMBERS { 89 | let id = TurkishId::from_str(number).unwrap(); 90 | let idstr = format!("{id}"); 91 | assert_eq!(idstr, *number); 92 | } 93 | } 94 | 95 | #[test] 96 | fn from_seq_produces_valid_numbers() { 97 | for number in VALID_NUMBERS { 98 | let seq: u32 = number[..9].parse().unwrap(); 99 | let id = TurkishId::from_seq(seq).unwrap(); 100 | assert_eq!(*number, id.to_string()); 101 | } 102 | } 103 | 104 | #[test] 105 | fn from_seq_out_of_range_values_return_error() { 106 | for seq in OUT_OF_RANGE_SEQUENCES { 107 | let result = TurkishId::from_seq(*seq); 108 | assert_eq!(result.err(), Some(FromSeqError::OutOfRange)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /trid/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides a `TurkishId` data type that holds a valid Turkish citizenship ID number. 2 | //! ID numbers can also be directly validated on `&str`'s by using `is_valid` function. 3 | //! 4 | //! # Examples 5 | //! 6 | //! Validate a Turkish citizenship ID number: 7 | //! 8 | //! ```rust 9 | //! if trid::is_valid("12345678901") { 10 | //! // yayyy! 11 | //! } 12 | //! ``` 13 | //! 14 | //! Try parsing a string into `TurkishId`: 15 | //! 16 | //! ```rust 17 | //! use trid::*; 18 | //! 19 | //! fn test() -> Result { 20 | //! let id : TurkishId = "12345678901".parse()?; 21 | //! Ok(id) 22 | //! } 23 | //! ``` 24 | //! 25 | //! # License 26 | //! 27 | //! Apache License Version 2.0, see LICENSE file for details. 28 | 29 | #![cfg_attr(not(test), no_std)] 30 | 31 | use core::{ 32 | convert::TryInto, 33 | fmt::{Display, Formatter}, 34 | ops::Range, 35 | str::{self, FromStr}, 36 | }; 37 | 38 | pub const LENGTH: usize = 11; 39 | 40 | /// Turkish citizenship ID number. The number is stored as ASCII digits 41 | /// "0".."9" in the structure. 42 | #[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)] 43 | pub struct TurkishId { 44 | id: [u8; LENGTH], 45 | } 46 | 47 | /// Represents the parser error for a given Turkish citizenship ID number. 48 | #[derive(Debug, Eq, PartialEq)] 49 | pub enum Error { 50 | /// The length of the ID isn't {LENGTH} 51 | InvalidLength, 52 | 53 | /// There's an invalid character in the ID string 54 | InvalidCharacter(char), 55 | 56 | /// The final checksum mismatches 57 | InvalidFinalChecksum, 58 | 59 | /// The initial checksum mismatches 60 | InvalidInitialChecksum, 61 | 62 | /// ID's first digit is zero 63 | FirstDigitIsZero, 64 | } 65 | 66 | /// Checks if the given string slice is a valid Turkish citizenship ID number. 67 | /// 68 | /// # Arguments 69 | /// 70 | /// * `value` - The string to check. 71 | /// 72 | /// # Returns 73 | /// 74 | /// `true` if the string is a valid Turkish ID number, `false` otherwise. 75 | /// 76 | /// # Example 77 | /// ``` 78 | /// use trid; 79 | /// 80 | /// assert!(trid::is_valid("76558242278")); 81 | /// ``` 82 | /// 83 | /// ``` 84 | /// use trid; 85 | /// 86 | /// assert!(!trid::is_valid("06558242278")); 87 | /// ``` 88 | #[must_use = "validity check must not be ignored"] 89 | pub fn is_valid(value: &str) -> bool { 90 | validate(value).is_ok() 91 | } 92 | 93 | /// Internal function to validate a given Turkish ID number. 94 | fn validate(str: &str) -> Result<(), Error> { 95 | /// Iterates over a char iterator and returns an i32 representing 96 | /// the next digit, or returns an error if the digit can't be parsed. 97 | fn next_digit(t: &mut impl Iterator) -> Result { 98 | let c = t.next().ok_or(Error::InvalidLength)?; 99 | 100 | // convert digit to u32 value 101 | c.to_digit(10) 102 | .and_then(|d| i32::try_from(d).ok()) // u32 -> i32 103 | .ok_or(Error::InvalidCharacter(c)) 104 | } 105 | 106 | if str.len() != LENGTH { 107 | return Err(Error::InvalidLength); 108 | } 109 | 110 | let mut digits = str.chars(); 111 | 112 | // start calculating checksums 113 | let mut odd_sum = next_digit(&mut digits)?; 114 | if odd_sum == 0 { 115 | // the first digit cannot be zero 116 | return Err(Error::FirstDigitIsZero); 117 | } 118 | 119 | let mut even_sum = 0; 120 | for _ in 0..4 { 121 | even_sum += next_digit(&mut digits)?; 122 | odd_sum += next_digit(&mut digits)?; 123 | } 124 | 125 | let first_checksum = next_digit(&mut digits)?; 126 | let final_checksum = next_digit(&mut digits)?; 127 | 128 | // we check for the final checksum first because it's computationally 129 | // cheaper. 130 | let final_checksum_computed = (odd_sum + even_sum + first_checksum) % 10; 131 | if final_checksum_computed != final_checksum { 132 | return Err(Error::InvalidFinalChecksum); 133 | } 134 | 135 | // we use euclidian remainder due to the possibility that the final 136 | // checksum wmight be negative. 137 | let first_checksum_computed = ((odd_sum * 7) - even_sum).rem_euclid(10); 138 | if first_checksum_computed != first_checksum { 139 | return Err(Error::InvalidInitialChecksum); 140 | } 141 | 142 | Ok(()) 143 | } 144 | 145 | /// TurkishId types are displayed as regular numbers. 146 | impl Display for TurkishId { 147 | fn fmt(&self, f: &mut Formatter) -> Result<(), core::fmt::Error> { 148 | write!( 149 | f, 150 | "{}", 151 | str::from_utf8(&self.id).map_err(|_| core::fmt::Error)? 152 | ) 153 | } 154 | } 155 | 156 | /// Error that describes the result of from_seq() 157 | #[derive(Debug, Eq, PartialEq)] 158 | pub enum FromSeqError { 159 | /// The sequence is out of the range of possible values 160 | OutOfRange, 161 | } 162 | 163 | impl TurkishId { 164 | pub const SEQ_RANGE: Range = 100_000_000..1_000_000_000; 165 | 166 | /// Generate a valid TurkishId from a sequence number by calculating 167 | /// checksums and building the buffer for it. 168 | /// 169 | /// # Arguments 170 | /// - seq - A number between 100,000,000 and 999,999,999 171 | /// 172 | /// # Returns 173 | /// A Result with `TurkishId` if the values are in range, otherwise 174 | /// `FromSeqError` 175 | pub fn from_seq(seq: u32) -> Result { 176 | fn to_ascii(digit: i32) -> u8 { 177 | digit as u8 + b'0' 178 | } 179 | if !TurkishId::SEQ_RANGE.contains(&seq) { 180 | return Err(FromSeqError::OutOfRange); 181 | } 182 | let mut d = [0; LENGTH]; 183 | let mut odd_sum: i32 = 0; 184 | let mut even_sum: i32 = 0; 185 | let mut divisor = Self::SEQ_RANGE.start; 186 | for (i, item) in d.iter_mut().enumerate().take(9) { 187 | let digit = (seq / divisor % 10) as i32; 188 | if i % 2 == 0 { 189 | odd_sum += digit; 190 | } else { 191 | even_sum += digit; 192 | } 193 | *item = to_ascii(digit); 194 | divisor /= 10; 195 | } 196 | let first_checksum = ((odd_sum * 7) - even_sum).rem_euclid(10); 197 | let second_checksum = (odd_sum + even_sum + first_checksum) % 10; 198 | d[9] = to_ascii(first_checksum); 199 | d[10] = to_ascii(second_checksum); 200 | Ok(TurkishId { id: d }) 201 | } 202 | } 203 | 204 | /// TurkishId can only be constructed from a string despite that it's stored 205 | /// as a fixed-length byte array internally. 206 | impl FromStr for TurkishId { 207 | type Err = Error; 208 | fn from_str(s: &str) -> Result { 209 | validate(s)?; 210 | let bytes = s.as_bytes().try_into().map_err(|_| Error::InvalidLength)?; 211 | let result = Self { id: bytes }; 212 | Ok(result) 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod tests; 218 | --------------------------------------------------------------------------------