use crate::prelude::*;
use crate::schema::namespaces::{DEFAULT_ORCID_SCHEMA_URI, DEFAULT_ROR_SCHEMA_URI};
use crate::util::constants::{
RE_ARK, RE_ARK_TEXT, RE_DOI, RE_DOI_TEXT, RE_ISBN, RE_ISBN_TEXT, RE_ORCID, RE_ORCID_TEXT, RE_RAID_TEXT, RE_ROR, RE_ROR_TEXT,
};
use crate::util::{base32_crockford_decode, regex_capture_lookup, ToStringChunks};
use bon::Builder;
use core::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub mod patent;
pub mod raid;
pub use patent::Patent;
const BETANUMERIC_DIGITS: &str = "0123456789bcdfghjkmnpqrstvwxz";
pub trait Betanumeric {
fn is_betanumeric(&self) -> bool {
false
}
fn to_betanumeric_ordinal(&self) -> Option<usize>;
}
pub trait PersistentIdentifier: fmt::Display {
fn new() -> Self;
fn schema_uri(&self) -> String;
fn identifier(&self) -> String;
fn prefix(&self) -> Option<String> {
None
}
fn suffix(&self) -> Option<String>;
fn check_digit(&self) -> Option<Vec<char>> {
None
}
fn url(&self) -> String {
String::new()
}
}
pub trait PersistentIdentifierConvert<T: AsRef<str>> {
fn format_as(&self, pid_type: PID) -> String;
fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal;
fn is_pid(&self, _pid_type: PID) -> bool;
fn is_ark(&self) -> bool;
fn is_doi(&self) -> bool;
fn is_isbn(&self) -> bool {
false
}
fn is_orcid(&self) -> bool;
fn is_raid(&self) -> bool;
fn is_ror(&self) -> bool;
}
pub trait PersistentIdentifierParse {
fn find_all(value: impl ToString) -> Vec<Self>
where
Self: Sized;
fn format(value: impl ToString) -> String;
fn from_string(value: impl ToString) -> Self
where
Self: Sized;
fn is_valid(value: impl ToString) -> bool;
}
#[derive(Default)]
pub struct PersistentIdentifierInternal {
value: String,
pid_type: PID,
}
#[derive(Clone, Debug, Default)]
pub enum PID {
#[default]
Unknown,
ARK,
DOI,
ISBN,
ORCID,
Patent,
PIDINST,
RAID,
ROR,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct ARK {
pub assigned_name: Option<String>,
#[builder(default = "ark:".to_string())]
pub label: String,
pub name_assigning_authority_number: Option<String>,
pub name_mapping_authority: Option<String>,
#[builder(default = Vec::new())]
pub parts: Vec<String>,
#[builder(default = Vec::new())]
pub variants: Vec<String>,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct DOI {
pub schema_uri: Option<String>,
pub directory_indicator: Option<String>,
pub registrant_code: Option<String>,
pub suffix: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[builder(start_fn = init, on(String, into))]
pub struct ISBN {
pub prefix_element: Option<String>,
pub registration_group: Option<String>,
pub publisher: Option<String>,
pub title: Option<String>,
pub check_digit: Option<String>,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct ORCID {
pub schema_uri: Option<String>,
pub identifier: Option<String>,
pub check_digit: Option<String>,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct RAID {
pub schema_uri: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub metadata: Option<raid::Metadata>,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct ROR {
pub schema_uri: Option<String>,
pub identifier: Option<String>,
pub check_digit: Option<String>,
}
impl Betanumeric for char {
fn is_betanumeric(&self) -> bool {
BETANUMERIC_DIGITS.contains(*self)
}
fn to_betanumeric_ordinal(&self) -> Option<usize> {
BETANUMERIC_DIGITS.chars().position(|x| x.eq(self))
}
}
impl Default for ARK {
fn default() -> Self {
Self::new()
}
}
impl Default for DOI {
fn default() -> Self {
Self::new()
}
}
impl Default for ORCID {
fn default() -> Self {
Self::new()
}
}
impl Default for RAID {
fn default() -> Self {
Self::new()
}
}
impl Default for ROR {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ARK {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let nma = self.name_mapping_authority.clone().unwrap_or_default().trim_end_matches('/').to_string();
let identifier = self.identifier();
let result = [nma, identifier].into_iter().filter(|x| !x.is_empty()).collect::<Vec<String>>().join("/");
write!(f, "{result}")
}
}
impl fmt::Display for DOI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.identifier();
write!(f, "{result}")
}
}
impl fmt::Display for ISBN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.identifier();
write!(f, "{result}")
}
}
impl fmt::Display for ORCID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let schema_uri = self.schema_uri();
let identifier = self.identifier();
let uri = if schema_uri.is_empty() { DEFAULT_ORCID_SCHEMA_URI } else { &schema_uri };
let values = match &self.identifier {
| Some(_) => [uri, &identifier].to_vec(),
| None => vec![],
};
let result = values
.into_iter()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/");
write!(f, "{result}")
}
}
impl fmt::Display for RAID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.identifier();
write!(f, "{result}")
}
}
impl fmt::Display for ROR {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let schema_uri = self.schema_uri();
let result = self.identifier();
if result.is_empty() {
write!(f, "")
} else {
write!(f, "{schema_uri}{result}")
}
}
}
impl PersistentIdentifier for ARK {
fn new() -> Self {
ARK::init().build()
}
fn schema_uri(&self) -> String {
let uri = match &self.name_mapping_authority {
| Some(value) => value,
| None => "",
};
uri.trim_end_matches("/").to_string()
}
fn identifier(&self) -> String {
let values = [self.prefix(), self.suffix()];
values
.iter()
.flatten()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/")
}
fn prefix(&self) -> Option<String> {
match (self.name_assigning_authority_number.as_ref(), self.assigned_name.as_ref()) {
| (Some(naan), Some(name)) => Some(format!("{}{}/{}", self.label.trim_end_matches('/'), naan, name)),
| _ => None,
}
}
fn suffix(&self) -> Option<String> {
let parts = self.parts.join("/");
let variants = self.variants.join(".");
let qualifiers = [parts, variants];
let result = qualifiers
.iter()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join(".");
Some(result)
}
fn check_digit(&self) -> Option<Vec<char>> {
let Self {
name_assigning_authority_number: naan,
assigned_name: name,
..
} = self;
let values = [naan.clone(), name.clone()];
if values.iter().all(|x| x.is_some()) {
let value = values.iter().flatten().map(String::from).collect::<Vec<String>>().join("/");
if value.is_empty() {
None
} else {
let trimmed = value.get(..value.len().saturating_sub(1)).unwrap_or_default();
noid_check_digit(trimmed)
}
} else {
None
}
}
}
impl PersistentIdentifier for DOI {
fn new() -> Self {
DOI::init().build()
}
fn schema_uri(&self) -> String {
self.schema_uri.as_ref().cloned().unwrap_or_default().trim_end_matches("/").to_string()
}
fn identifier(&self) -> String {
let values = [self.prefix(), self.suffix()];
values
.iter()
.flatten()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/")
}
fn prefix(&self) -> Option<String> {
let values = [
self.directory_indicator.as_ref().cloned().unwrap_or_default(),
self.registrant_code.as_ref().cloned().unwrap_or_default(),
];
let result = values
.iter()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join(".");
Some(result)
}
fn suffix(&self) -> Option<String> {
fn postprocess(mut value: String) -> String {
if value.ends_with(".") {
value.pop();
}
value
}
let result = self.suffix.as_ref().cloned().unwrap_or_default();
if !result.is_empty() {
Some(postprocess(result))
} else {
None
}
}
fn url(&self) -> String {
let identifier = self.identifier();
if identifier.is_empty() {
String::new()
} else {
let uri = self.schema_uri();
let schema = if uri.is_empty() { "https://doi.org" } else { &uri };
format!("{}/{}", schema, identifier)
}
}
}
impl PersistentIdentifier for ISBN {
fn new() -> Self {
ISBN::init().build()
}
fn schema_uri(&self) -> String {
"".to_string()
}
fn identifier(&self) -> String {
let ISBN {
prefix_element,
registration_group,
publisher,
title,
check_digit,
} = self;
[prefix_element, registration_group, publisher, title, check_digit]
.into_iter()
.map(|x| x.clone().unwrap_or_default())
.collect::<Vec<String>>()
.join("-")
}
fn prefix(&self) -> Option<String> {
let ISBN {
prefix_element,
registration_group,
publisher,
..
} = self;
let result = format!(
"{}.{}{}",
prefix_element.clone().unwrap_or_default(),
registration_group.clone().unwrap_or_default(),
publisher.clone().unwrap_or_default()
);
Some(result)
}
fn suffix(&self) -> Option<String> {
let ISBN { title, check_digit, .. } = self;
let result = [title, check_digit]
.into_iter()
.map(|x| x.clone().unwrap_or_default())
.collect::<Vec<String>>()
.join("");
Some(result)
}
fn check_digit(&self) -> Option<Vec<char>> {
isbn_check_digit(self.identifier())
}
}
impl From<ISBN> for DOI {
fn from(isbn: ISBN) -> Self {
DOI::init()
.schema_uri("https://doi.org/")
.directory_indicator("10")
.maybe_registrant_code(isbn.prefix())
.maybe_suffix(isbn.suffix())
.build()
}
}
impl From<DOI> for ISBN {
fn from(doi: DOI) -> Self {
let prefix = doi.prefix().unwrap_or_default().replace(".", "-");
let suffix = match doi.suffix() {
| Some(value) => {
let check_digit = value.chars().last().unwrap_or_default().to_string();
let title = value.get(..value.len().saturating_sub(1)).unwrap_or_default().to_string();
format!("{title}-{check_digit}")
}
| None => "".to_string(),
};
let result = format!("{}-{suffix}", prefix.trim_start_matches("10-"));
ISBN::from_string(result)
}
}
impl PersistentIdentifier for ORCID {
fn new() -> Self {
ORCID::init().build()
}
fn schema_uri(&self) -> String {
self.schema_uri.as_ref().cloned().unwrap_or_default().trim_end_matches("/").to_string()
}
fn identifier(&self) -> String {
let stripped = self.identifier.as_ref().cloned().unwrap_or_default().replace("-", "");
stripped.chunk(4).join("-")
}
fn suffix(&self) -> Option<String> {
Some(self.identifier())
}
fn check_digit(&self) -> Option<Vec<char>> {
orcid_check_digit(self.identifier())
}
}
impl PersistentIdentifier for RAID {
fn new() -> Self {
RAID::init().build()
}
fn schema_uri(&self) -> String {
self.schema_uri.as_ref().cloned().unwrap_or_default().trim_end_matches("/").to_string()
}
fn prefix(&self) -> Option<String> {
self.prefix.clone()
}
fn suffix(&self) -> Option<String> {
self.suffix.clone()
}
fn identifier(&self) -> String {
let values = [self.prefix(), self.suffix()];
values
.iter()
.flatten()
.filter(|x| !x.is_empty())
.map(String::from)
.collect::<Vec<String>>()
.join("/")
}
}
impl PersistentIdentifier for ROR {
fn new() -> Self {
ROR::init().build()
}
fn schema_uri(&self) -> String {
let processed = self
.schema_uri
.as_ref()
.cloned()
.unwrap_or_else(|| DEFAULT_ROR_SCHEMA_URI.to_string())
.trim_end_matches("/")
.replace(" ", "")
.to_string();
format!("{processed}/")
}
fn identifier(&self) -> String {
self.identifier.clone().unwrap_or_default()
}
fn suffix(&self) -> Option<String> {
self.identifier.clone()
}
fn check_digit(&self) -> Option<Vec<char>> {
ror_check_digit(&self.identifier()[1..])
}
}
impl<T: AsRef<str>> PersistentIdentifierConvert<T> for T
where
T: ToString,
{
fn format_as(&self, pid_type: PID) -> String {
match pid_type {
| PID::ARK => ARK::format(self.as_ref()),
| PID::DOI => DOI::format(self.as_ref()),
| PID::ORCID => ORCID::format(self.as_ref()),
| PID::RAID => RAID::format(self.as_ref()),
| PID::ROR => <ROR as PersistentIdentifierParse>::format(self.as_ref()),
| _ => self.as_ref().to_string(),
}
}
fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal {
let value = self.as_ref().to_string();
match pid_type {
| PID::ARK => PersistentIdentifierInternal { value, pid_type: PID::ARK },
| PID::DOI => PersistentIdentifierInternal { value, pid_type: PID::DOI },
| PID::ORCID => PersistentIdentifierInternal { value, pid_type: PID::ORCID },
| PID::RAID => PersistentIdentifierInternal { value, pid_type: PID::RAID },
| PID::ROR => PersistentIdentifierInternal { value, pid_type: PID::ROR },
| _ => PersistentIdentifierInternal::default(),
}
}
fn is_pid(&self, pid_type: PID) -> bool {
match pid_type {
| PID::ARK => self.is_ark(),
| PID::DOI => self.is_doi(),
| PID::ORCID => self.is_orcid(),
| PID::RAID => self.is_raid(),
| PID::ROR => self.is_ror(),
| _ => false,
}
}
fn is_ark(&self) -> bool {
ARK::is_valid(self.as_ref())
}
fn is_doi(&self) -> bool {
DOI::is_valid(self.as_ref())
}
fn is_isbn(&self) -> bool {
ISBN::is_valid(self.as_ref())
}
fn is_orcid(&self) -> bool {
ORCID::is_valid(self.as_ref())
}
fn is_raid(&self) -> bool {
RAID::is_valid(self.as_ref())
}
fn is_ror(&self) -> bool {
ROR::is_valid(self.as_ref())
}
}
impl PersistentIdentifierInternal {
pub fn to_ark(&self) -> ARK {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ARK => ARK::from_string(value),
| _ => ARK::default(),
}
}
pub fn to_doi(&self) -> DOI {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::DOI => DOI::from_string(value),
| _ => DOI::default(),
}
}
pub fn to_orcid(&self) -> ORCID {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ORCID => ORCID::from_string(value),
| _ => ORCID::default(),
}
}
pub fn to_raid(&self) -> RAID {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::RAID => RAID::from_string(value),
| _ => RAID::default(),
}
}
pub fn to_ror(&self) -> ROR {
let PersistentIdentifierInternal { value, pid_type } = self;
match pid_type {
| PID::ROR => ROR::from_string(value),
| _ => ROR::default(),
}
}
}
impl PersistentIdentifierParse for ARK {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_ARK;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| ARK::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
ARK::from_string(value.to_string()).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["nma", "label", "naan", "assigned_name", "parts", "variants"];
let pattern = format!("^{RE_ARK_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
let parts = match lookup.get("parts") {
| Some(value) => value.split('/').map(String::from).collect(),
| None => vec![],
};
let variants = match lookup.get("variants") {
| Some(value) => value.split('.').map(String::from).collect(),
| None => vec![],
};
ARK::init()
.maybe_assigned_name(lookup.get("assigned_name").cloned())
.maybe_label(lookup.get("label").cloned())
.maybe_name_assigning_authority_number(lookup.get("naan").cloned())
.maybe_name_mapping_authority(lookup.get("nma").cloned())
.parts(parts)
.variants(variants)
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = ARK::from_string(value);
let naan = pid.name_assigning_authority_number.unwrap_or_default();
let naan_is_betanumeric = naan.chars().all(|x| x.is_betanumeric());
let shoulder_starts_with_lowercase_letter = match pid.assigned_name {
| Some(value) => match value.chars().next() {
| Some(value) => value.is_ascii_lowercase() && !value.eq(&'l'),
| None => false,
},
| None => false,
};
!naan.is_empty() && naan_is_betanumeric && shoulder_starts_with_lowercase_letter
}
}
impl PersistentIdentifierParse for DOI {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_DOI;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| DOI::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
DOI::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "directory_indicator", "prefix_element", "registrant_code", "suffix"];
let pattern = format!("^{RE_DOI_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
DOI::init()
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_directory_indicator(lookup.get("directory_indicator").cloned())
.maybe_registrant_code(lookup.get("registrant_code").cloned())
.maybe_suffix(lookup.get("suffix").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = DOI::from_string(value.to_string());
let prefix_is_valid = match pid.prefix() {
| Some(x) => is_numeric(&x.replace(".", "")) && !x.eq("10.5555"),
| _ => false,
};
let suffix_is_valid = pid.suffix().is_some();
prefix_is_valid && suffix_is_valid
}
}
impl PersistentIdentifierParse for ISBN {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_ISBN;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| ISBN::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
ISBN::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["prefix_element", "registration_group", "publisher", "title", "check_digit"];
let pattern = format!("^{RE_ISBN_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
ISBN::init()
.maybe_prefix_element(lookup.get("prefix_element").cloned())
.maybe_registration_group(lookup.get("registration_group").cloned())
.maybe_publisher(lookup.get("publisher").cloned())
.maybe_title(lookup.get("title").cloned())
.maybe_check_digit(lookup.get("check_digit").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = ISBN::from_string(value.to_string());
let last = value.to_string().chars().last().unwrap_or_default();
let has_valid_check_digit = match pid.check_digit() {
| Some(chars) => chars.contains(&last),
| _ => false,
};
let is_valid_length = value.to_string().replace("-", "").len() == 13;
has_valid_check_digit && is_valid_length
}
}
impl PersistentIdentifierParse for ORCID {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_ORCID;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| ORCID::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
ORCID::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "identifier", "check_digit"];
let pattern = format!("^{RE_ORCID_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
ORCID::init()
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_identifier(lookup.get("identifier").cloned())
.maybe_check_digit(lookup.get("check_digit").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = ORCID::from_string(value.to_string());
let identifier = pid.identifier();
let last = identifier.chars().last().unwrap_or_default();
match orcid_check_digit(identifier.as_str()) {
| Some(check_digit) => {
if check_digit.contains(&last) {
identifier.len() == 19
} else {
false
}
}
| _ => false,
}
}
}
impl PersistentIdentifierParse for RAID {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_DOI;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| RAID::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
RAID::from_string(value).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "directory_indicator", "registrant_code", "suffix"];
let pattern = format!("^{RE_RAID_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
let directory_indicator = lookup.get("directory_indicator").cloned();
let registrant_code = lookup.get("registrant_code").cloned();
let prefix = [directory_indicator, registrant_code]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join(".");
RAID::init()
.prefix(prefix)
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_suffix(lookup.get("suffix").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = RAID::from_string(value.to_string());
let prefix_is_valid = match pid.prefix() {
| Some(x) => is_numeric(&x.replace(".", "")) && !x.eq("10.5555"),
| _ => false,
};
let suffix_is_valid = pid.suffix().is_some();
prefix_is_valid && suffix_is_valid
}
}
impl PersistentIdentifierParse for ROR {
fn find_all(value: impl ToString) -> Vec<Self> {
let re = &RE_ROR;
re.find_iter(&value.to_string())
.filter_map(Result::ok)
.map(|m| ROR::from_string(m.as_str()))
.collect()
}
fn format(value: impl ToString) -> String {
ROR::from_string(value.to_string()).to_string()
}
fn from_string(value: impl ToString) -> Self {
let groups = ["schema_uri", "identifier", "check_digit"];
let pattern = format!("^{RE_ROR_TEXT}$");
let text = value.to_string();
let lookup = regex_capture_lookup(pattern.as_ref(), text.as_ref(), groups.to_vec());
ROR::init()
.maybe_schema_uri(lookup.get("schema_uri").cloned())
.maybe_identifier(lookup.get("identifier").cloned())
.maybe_check_digit(lookup.get("check_digit").cloned())
.build()
}
fn is_valid(value: impl ToString) -> bool {
let pid = ROR::from_string(value.to_string());
let identifier = pid.identifier();
let last_two = identifier.chars().rev().take(2).collect::<String>().chars().rev().collect::<String>();
if identifier.is_empty() {
false
} else {
match ror_check_digit(&identifier[1..]) {
| Some(check_digit) => {
if identifier.len() == 9 {
let calculated_last_two = check_digit.iter().collect::<String>();
calculated_last_two == last_two
} else {
false
}
}
| _ => false,
}
}
}
}
#[allow(clippy::arithmetic_side_effects)]
pub fn isbn_check_digit<S>(_value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const MODULUS: u32 = 10;
let working = _value.as_ref().replace("-", "");
let sum = working.chars().take(12).enumerate().fold(0, |acc, (index, x)| {
let digit = x.to_digit(10).unwrap_or_default();
let multiplier = if index % 2 == 0 { 1 } else { 3 };
acc + (digit * multiplier)
});
let remainder = sum % MODULUS;
let result = if remainder == 0 { 0 } else { MODULUS - remainder };
char::from_digit(result, 10).map(|c| vec![c])
}
#[allow(clippy::arithmetic_side_effects)]
pub fn noid_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const RADIX: usize = 29;
let sum = value.as_ref().chars().enumerate().fold(0, |acc, (i, val)| {
let position = i + 1;
let ordinal = val.to_betanumeric_ordinal().unwrap_or(0);
acc + (position * ordinal)
});
let remainder = sum % RADIX;
to_betanumeric(remainder as u8).map(|c| vec![c])
}
#[allow(clippy::arithmetic_side_effects)]
pub fn orcid_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const MODULUS: u32 = 11;
const RADIX: u32 = 2;
let working = value.as_ref().replace("-", "").replace(" ", "");
let sum = working.chars().take(15).fold(0, |acc, x| {
let digit = x.to_digit(10).unwrap_or_default();
(acc + digit) * RADIX
});
let remainder = sum % MODULUS;
let result = (MODULUS + 1 - remainder) % MODULUS;
if result == 10 {
Some(vec!['X'])
} else {
char::from_digit(result, 10).map(|c| vec![c])
}
}
#[allow(clippy::arithmetic_side_effects)]
pub fn ror_check_digit<S>(value: S) -> Option<Vec<char>>
where
S: AsRef<str>,
{
const MODULUS: u128 = 97;
let working = value
.as_ref()
.replace("-", "")
.replace(" ", "")
.chars()
.take(6)
.map(String::from)
.collect::<Vec<_>>()
.join("");
match base32_crockford_decode(working) {
| Some(value) => {
let remainder = (value * 100) % MODULUS;
let checksum = (MODULUS + 1 - remainder) % MODULUS;
let result = if checksum < 10 {
format!("0{}", checksum).chars().collect()
} else {
checksum.to_string().chars().collect()
};
Some(result)
}
| None => None,
}
}
fn to_betanumeric(value: u8) -> Option<char> {
match BETANUMERIC_DIGITS.chars().enumerate().find(|(i, _)| *i == value as usize) {
| Some((_, x)) => Some(x),
| None => None,
}
}
fn is_numeric(value: &str) -> bool {
value.chars().all(|x| x.is_numeric())
}
#[cfg(test)]
mod tests;