Skip to main content

acorn/schema/pid/
mod.rs

1//! Persistent Identifiers (PID)
2//!
3//! Contains functions for working with persistent identifiers (PID) such as [`ORCID`], [`DOI`]s, and [RAiD](`raid`)s
4//!
5//! ### Features
6//! - Best in class validation
7//! - Convert persistent identifiers into standard formats
8//! - Access the sub parts of a persistent identifier
9//!
10//! [RAiDs]: https://www.raid.org/
11use crate::prelude::*;
12use crate::util::constants::{HTTP_URL, RE_ARXIV};
13use crate::util::{base32_crockford_decode, trim_unmatched_trailing_parentheses};
14use bon::Builder;
15use core::fmt;
16#[cfg(feature = "std")]
17use data_encoding::HEXLOWER;
18#[cfg(feature = "std")]
19use ring::digest::{digest, SHA256};
20use serde::{Deserialize, Serialize};
21use strum::{EnumIs, EnumIter, IntoEnumIterator};
22
23pub mod ark;
24pub mod arxiv;
25pub mod doi;
26pub mod handle;
27pub mod isbn;
28pub mod isni;
29pub mod orcid;
30pub mod patent;
31pub mod raid;
32pub mod ror;
33pub mod swhid;
34
35pub use ark::ARK;
36pub use arxiv::Arxiv;
37pub use doi::DOI;
38pub use handle::Handle;
39pub use isbn::ISBN;
40pub use isni::ISNI;
41pub use orcid::ORCID;
42pub use patent::Patent;
43pub use raid::RAID;
44pub use ror::ROR;
45pub use swhid::SWHID;
46
47const BETANUMERIC_DIGITS: &str = "0123456789bcdfghjkmnpqrstvwxz";
48
49/// Add utility functions for working with beta numeric values
50///
51/// Mostly intended for working with [NCDA](`noid_check_digit`)
52pub trait Betanumeric {
53    /// Check if `self` is a betanumeric value
54    fn is_betanumeric(&self) -> bool {
55        false
56    }
57    /// Convert `self` into a betanumeric ordinal value
58    /// ### Example
59    /// > `w` -> `26`
60    fn to_betanumeric_ordinal(&self) -> Option<usize>;
61}
62/// Provides common functions for working with persistent identifiers (PID)
63pub trait PersistentIdentifier: fmt::Display {
64    /// Create a new PID
65    fn new() -> Self;
66    /// Get standardized form of schema URI for a PID
67    /// ### Examples
68    /// - `https://doi.org`
69    /// - `https://orcid.org`
70    fn schema_uri(&self) -> String;
71    /// Get PID identifier section
72    /// ### Examples
73    /// - `ark:1234/x5678` for [`ARK`]
74    /// - `10.1234/5678` for [`DOI`]
75    /// - `0000-0002-2057-9115` for [`ORCID`]
76    fn identifier(&self) -> String;
77    /// Get PID prefix (different interpretation depending on PID type)
78    ///
79    /// Not every PID type has a prefix, but generally every PID has a "first" part that can losely be considered a "prefix"
80    fn prefix(&self) -> Option<String> {
81        None
82    }
83    /// Get PID suffix (different interpretation depending on PID type)
84    ///
85    /// Not every PID type has a suffix, but generally every PID has a "second" part that can losely be considered a "suffix"
86    fn suffix(&self) -> Option<String>;
87    /// Get PID check digit (when applicable)
88    fn check_digit(&self) -> Option<Vec<char>> {
89        None
90    }
91    /// Get fully resolved URL of the PID with its schema URI
92    fn url(&self) -> String {
93        String::new()
94    }
95}
96/// Add coercion to persistent identifier (PID) functionality to string values
97pub trait PersistentIdentifierConvert<T: AsRef<str>> {
98    /// Convert `self` into a string standard format PID of a certain type
99    /// ```ignore
100    /// use acorn::schema::pid::{PID, PersistentIdentifier};
101    ///
102    /// assert_eq!("https://doi.org/10.1234/5678".format_as(PID::DOI), "10.1234/5678");
103    /// assert_eq!("0000-0002-2057-9115".format_as(PID::ORCID), "https://orcid.org/0000-0002-2057-9115");
104    /// ```
105    fn format_as(&self, pid_type: PID) -> String;
106    /// Coerce `self` into given PID type.
107    /// ```ignore
108    /// use acorn::schema::pid::{PID, PersistentIdentifier};
109    ///
110    /// let doi = "https://doi.org/10.1234/5678".to_pid(PID::DOI).to_doi();
111    /// assert_eq!(doi.suffix(), "5678");
112    /// ```
113    fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal;
114    /// Determines if `self` is of the given PID type.
115    /// ```ignore
116    /// use acorn::schema::pid::{PID, PersistentIdentifier};
117    ///
118    /// assert!("https://doi.org/10.1234/5678".is_pid(PID::DOI));
119    /// ```
120    fn is_pid(&self, _pid_type: PID) -> bool;
121    /// Determines if `self` is an archival resource key (ARK)
122    /// ```ignore
123    /// use acorn::schema::pid::{PID, PersistentIdentifier};
124    ///
125    /// assert!("https://n2t.net/ark:12148/btv1b8449691v/f29".is_ark());
126    /// ```
127    fn is_ark(&self) -> bool;
128    /// Determines if `self` is an arXiv identifier
129    fn is_arxiv(&self) -> bool;
130    /// Determines if `self` is a DOI
131    /// ```ingore
132    /// use acorn::schema::pid::{PID, PersistentIdentifier};
133    ///
134    /// assert!("https://doi.org/10.1234/5678".is_doi());
135    /// ```
136    fn is_doi(&self) -> bool;
137    /// Determines if `self` is a Handle identifier.
138    fn is_handle(&self) -> bool;
139    /// Determines if `self` is an ISBN
140    fn is_isbn(&self) -> bool {
141        false
142    }
143    /// Determines if `self` is an ISNI
144    fn is_isni(&self) -> bool;
145    /// Determines if `self` is a ORCID
146    /// ```ignore
147    /// use acorn::schema::pid::{PID, PersistentIdentifier};
148    ///
149    /// assert!("https://orcid.org/0000-0000-0000-0000".is_orcid());
150    /// ```
151    fn is_orcid(&self) -> bool;
152    /// Determines if `self` is a RAID
153    /// ```ignore
154    /// use acorn::schema::pid::{PID, PersistentIdentifier};
155    ///
156    /// assert!("https://raid.org/10.83962/fb5be317".is_raid());
157    /// ```````
158    fn is_raid(&self) -> bool;
159    /// Determines if `self` is a ROR
160    /// ```ignore
161    /// use acorn::schema::pid::{PID, PersistentIdentifier};
162    ///
163    /// assert!("https://ror.org/01qz5mb56".is_ror());
164    /// ```
165    fn is_ror(&self) -> bool;
166    /// Determines if `self` is a Software Hash Identifier.
167    fn is_swhid(&self) -> bool;
168}
169/// Trait for working with persistent identifiers (PID) as and within string values
170pub trait PersistentIdentifierParse {
171    /// Find all PID values present in a string
172    fn find_all(value: impl ToString) -> Vec<Self>
173    where
174        Self: Sized;
175    /// Parse and format a PID according to its associated canonical format
176    fn format(value: impl ToString) -> String;
177    /// Instantiate a PID from a string
178    fn from_string(value: impl ToString) -> Self
179    where
180        Self: Sized;
181    /// Determine if a string is a valid PID
182    fn is_valid(value: impl ToString) -> bool;
183}
184/// Persistent Identifier (PID) types
185///
186/// PIDs are globally unique identifiers, resolvable on the Web, and associated with a set of additional descriptive metadata (ex. [`raid::Metadata`])
187#[derive(Clone, Debug, Default, EnumIs, EnumIter, Eq, Ord, PartialEq, PartialOrd)]
188pub enum PID {
189    /// Unknown PID
190    #[default]
191    Unknown,
192    /// Archival Resource Key (ARK)
193    ///
194    /// Widely used persistent identifier, supported by the California Digital Library \[21\], in collaboration with DuraSpaceď‚…. ARKs work similarly to DOIs, but are more permissive in design.[^ark]
195    ///
196    /// [^ark]: `M. Stocker et al., "Persistent Identification of Instruments," Data Science Journal, vol. 19, p. 18, May 2020, doi: 10.5334/dsj-2020-018.`
197    ARK,
198    /// arXiv identifier
199    ///
200    /// See [`Arxiv`]
201    Arxiv,
202    /// Digital Object Identifier (DOI)
203    ///
204    /// See [`DOI`]
205    DOI,
206    /// Handle System identifier
207    ///
208    /// See [`Handle`]
209    Handle,
210    /// International Standard Book Number (ISBN)
211    ///
212    /// See [`ISBN`]
213    ISBN,
214    /// International Standard Name Identifier (ISNI)
215    ///
216    /// See [`ISNI`]
217    ISNI,
218    /// Open Researcher and Contributor ID (ORCiD)
219    ///
220    /// See [`ORCID`]
221    ORCID,
222    /// Patent Number
223    Patent,
224    /// Persistent Identification of Instruments (PIDINST)
225    /// ### Citation
226    /// ```text
227    /// M. Stocker et al., "Persistent Identification of Instruments," Data Science Journal, vol. 19, p. 18, May 2020, doi: 10.5334/dsj-2020-018.
228    /// ```
229    PIDINST,
230    /// Research Activity Identifier (RAiD)
231    ///
232    /// Developed by tthe Australian Research Data Commons (ARDC), used to identify research projects and activities for access by research communities worldwide
233    ///
234    /// The ARDC and [DataCite](https://datacite.org/) have entered an agreement to use DataCite [`DOI`]s as RAiD identifiers
235    ///
236    /// See [`raid`] module
237    RAID,
238    /// Research Organization Registry (ROR)
239    ///
240    /// Global, community-led registry of open persistent identifiers for research organizations
241    ///
242    /// See <https://www.ror.org/> for more information
243    ROR,
244    /// Software Hash Identifier (SWHID)
245    ///
246    /// Intrinsic identifier for software content, directories, revisions, releases, and snapshots.
247    SWHID,
248    /// Public HTTP or HTTPS artifact URL
249    URL,
250}
251/// DOI or arXiv publication identifier type.
252#[derive(Clone, Debug)]
253pub enum PublicationIdentifierType {
254    /// Digital Object Identifier
255    Doi(DOI),
256    /// arXiv identifier
257    Arxiv(Arxiv),
258    /// Unsupported publication identifier
259    Unknown,
260}
261/// A persistent identifier type paired with its value.
262#[derive(Builder, Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
263#[builder(start_fn = init, on(String, into))]
264pub struct Identifier {
265    /// Persistent identifier type.
266    pub kind: PID,
267    /// Identifier value, either raw or normalized.
268    pub value: String,
269}
270/// Internal representation of a persistent identifier
271#[derive(Default)]
272pub struct PersistentIdentifierInternal {
273    /// Raw string content of the (possible) PID
274    value: String,
275    /// Type of PID
276    pid_type: PID,
277}
278impl Identifier {
279    /// Create an identifier with an unknown type.
280    pub fn new(value: impl Into<String>) -> Self {
281        Self {
282            kind: PID::Unknown,
283            value: value.into(),
284        }
285    }
286    /// Normalize this identifier as one supported persistent or URL artifact identifier.
287    pub fn normalized(&self) -> Option<Self> {
288        let trimmed = self
289            .value
290            .trim()
291            .trim_matches(|character: char| matches!(character, '<' | '>' | '(' | ')' | '[' | ']' | ',' | ';'));
292        let trimmed = trim_unmatched_trailing_parentheses(trimmed);
293        match self.kind {
294            | PID::ARK => Self::parsed::<ARK>(PID::ARK, trimmed),
295            | PID::Arxiv => Self::parsed::<Arxiv>(PID::Arxiv, trimmed),
296            | PID::DOI => Self::parsed::<DOI>(PID::DOI, trimmed),
297            | PID::Handle => Self::parsed::<Handle>(PID::Handle, trimmed),
298            | PID::ISBN => Self::parsed::<ISBN>(PID::ISBN, trimmed),
299            | PID::ISNI => Self::parsed::<ISNI>(PID::ISNI, trimmed),
300            | PID::ORCID => Self::parsed::<ORCID>(PID::ORCID, trimmed),
301            | PID::Patent => Self::parsed::<Patent>(PID::Patent, trimmed),
302            | PID::RAID => Self::parsed::<RAID>(PID::RAID, trimmed),
303            | PID::ROR => Self::parsed::<ROR>(PID::ROR, trimmed),
304            | PID::SWHID => Self::parsed::<SWHID>(PID::SWHID, trimmed),
305            | PID::URL if HTTP_URL.is_match(trimmed).unwrap_or(false) => Some(Self {
306                kind: PID::URL,
307                value: trimmed.trim_end_matches('/').to_string(),
308            }),
309            | PID::Unknown => {
310                let lowercase = trimmed.to_ascii_lowercase();
311                let primary = if lowercase.starts_with("raid:") || lowercase.starts_with("https://raid.org/") {
312                    PID::RAID
313                } else {
314                    PID::DOI
315                };
316                [
317                    PID::Arxiv,
318                    primary,
319                    PID::ARK,
320                    PID::Handle,
321                    PID::ISBN,
322                    PID::ORCID,
323                    PID::ISNI,
324                    PID::Patent,
325                    PID::ROR,
326                    PID::SWHID,
327                    PID::URL,
328                ]
329                .into_iter()
330                .find_map(|kind| {
331                    Self {
332                        kind,
333                        value: self.value.clone(),
334                    }
335                    .normalized()
336                })
337            }
338            | _ => None,
339        }
340    }
341    /// Normalize an exact project identity key.
342    pub fn normalize(value: &str) -> String {
343        let value = value.trim();
344        match value.split_once(':') {
345            | Some((prefix, identifier)) => {
346                let prefix = prefix.to_ascii_lowercase();
347                let kind = PID::from(prefix.as_str());
348                let lowercase = kind.is_arxiv() || kind.is_doi() || kind.is_raid() || kind.is_isbn() || kind.is_patent() || kind.is_swhid();
349                if lowercase {
350                    format!("{prefix}:{}", identifier.trim().to_ascii_lowercase())
351                } else {
352                    format!("{prefix}:{}", identifier.trim())
353                }
354            }
355            | None => value.to_string(),
356        }
357    }
358    /// Return this identifier as a normalized exact project identity key.
359    pub fn identity_key(&self) -> String {
360        match self.kind {
361            | PID::Arxiv => {
362                let identifier = Arxiv::from_string(&self.value).work_identifier();
363                Self::normalize(&format!("arxiv:{}", identifier.trim_start_matches("arXiv:")))
364            }
365            | PID::SWHID => Self::normalize(&format!("swhid:{}", SWHID::from_string(&self.value).core_identifier())),
366            | _ => Self::normalize(&format!("{}:{}", self.kind.as_str(), self.value)),
367        }
368    }
369    fn parsed<T: PersistentIdentifierParse + fmt::Display>(kind: PID, value: &str) -> Option<Self> {
370        let formatted = T::format(value);
371        match T::is_valid(&formatted) {
372            | true => Some(Self { kind, value: formatted }),
373            | false => T::find_all(value)
374                .first()
375                .map(T::format)
376                .filter(|value| T::is_valid(value))
377                .map(|value| Self { kind, value }),
378        }
379    }
380    /// Return the deterministic identifier hash used in artifact paths
381    #[cfg(feature = "std")]
382    pub fn identifier_hash(&self) -> String {
383        HEXLOWER.encode(digest(&SHA256, self.value.as_bytes()).as_ref())[..12].to_string()
384    }
385}
386impl<'a> From<&'a Identifier> for &'a str {
387    fn from(identifier: &'a Identifier) -> Self {
388        identifier.kind.as_str()
389    }
390}
391impl From<&str> for Identifier {
392    fn from(value: &str) -> Self {
393        Self::new(value)
394    }
395}
396impl From<ARK> for Identifier {
397    fn from(value: ARK) -> Self {
398        Self {
399            kind: PID::ARK,
400            value: value.to_string(),
401        }
402    }
403}
404impl From<Arxiv> for Identifier {
405    fn from(value: Arxiv) -> Self {
406        Self {
407            kind: PID::Arxiv,
408            value: value.to_string(),
409        }
410    }
411}
412impl From<DOI> for Identifier {
413    fn from(value: DOI) -> Self {
414        Self {
415            kind: PID::DOI,
416            value: value.to_string(),
417        }
418    }
419}
420impl From<Handle> for Identifier {
421    fn from(value: Handle) -> Self {
422        Self {
423            kind: PID::Handle,
424            value: value.to_string(),
425        }
426    }
427}
428impl From<ISBN> for Identifier {
429    fn from(value: ISBN) -> Self {
430        Self {
431            kind: PID::ISBN,
432            value: value.to_string(),
433        }
434    }
435}
436impl From<ISNI> for Identifier {
437    fn from(value: ISNI) -> Self {
438        Self {
439            kind: PID::ISNI,
440            value: value.to_string(),
441        }
442    }
443}
444impl From<ORCID> for Identifier {
445    fn from(value: ORCID) -> Self {
446        Self {
447            kind: PID::ORCID,
448            value: value.to_string(),
449        }
450    }
451}
452impl From<Patent> for Identifier {
453    fn from(value: Patent) -> Self {
454        Self {
455            kind: PID::Patent,
456            value: value.to_string(),
457        }
458    }
459}
460impl From<RAID> for Identifier {
461    fn from(value: RAID) -> Self {
462        Self {
463            kind: PID::RAID,
464            value: value.to_string(),
465        }
466    }
467}
468impl From<ROR> for Identifier {
469    fn from(value: ROR) -> Self {
470        Self {
471            kind: PID::ROR,
472            value: value.to_string(),
473        }
474    }
475}
476impl From<SWHID> for Identifier {
477    fn from(value: SWHID) -> Self {
478        Self {
479            kind: PID::SWHID,
480            value: value.to_string(),
481        }
482    }
483}
484impl PID {
485    /// Whether this PID type participates in artifact discovery
486    pub fn is_discoverable(&self) -> bool {
487        self.is_ark()
488            || self.is_arxiv()
489            || self.is_doi()
490            || self.is_handle()
491            || self.is_isbn()
492            || self.is_isni()
493            || self.is_orcid()
494            || self.is_patent()
495            || self.is_raid()
496            || self.is_ror()
497            || self.is_swhid()
498            || self.is_url()
499    }
500    /// Return the stable lowercase path component
501    pub fn as_str(&self) -> &'static str {
502        match self {
503            | Self::ARK => "ark",
504            | Self::Arxiv => "arxiv",
505            | Self::DOI => "doi",
506            | Self::Handle => "handle",
507            | Self::ISBN => "isbn",
508            | Self::ISNI => "isni",
509            | Self::ORCID => "orcid",
510            | Self::Patent => "patent",
511            | Self::PIDINST => "pidinst",
512            | Self::RAID => "raid",
513            | Self::ROR => "ror",
514            | Self::SWHID => "swhid",
515            | Self::URL => "url",
516            | _ => "unknown",
517        }
518    }
519    /// Whether this PID type can independently identify a research project.
520    pub fn is_project_identifier(&self) -> bool {
521        self.is_doi() || self.is_arxiv() || self.is_raid() || self.is_isbn() || self.is_patent() || self.is_ark() || self.is_swhid()
522    }
523}
524impl fmt::Display for PID {
525    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
526        formatter.write_str(self.as_str())
527    }
528}
529impl Serialize for PID {
530    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
531    where
532        S: serde::Serializer,
533    {
534        serializer.serialize_str(self.as_str())
535    }
536}
537impl<'de> Deserialize<'de> for PID {
538    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
539    where
540        D: serde::Deserializer<'de>,
541    {
542        String::deserialize(deserializer).and_then(|value| {
543            Self::iter()
544                .find(|pid| pid.as_str().eq_ignore_ascii_case(&value))
545                .ok_or_else(|| serde::de::Error::custom(format!("unknown PID type `{value}`")))
546        })
547    }
548}
549impl From<&str> for PID {
550    fn from(value: &str) -> Self {
551        Self::iter()
552            .find(|pid| pid.as_str().eq_ignore_ascii_case(value.trim()))
553            .unwrap_or_default()
554    }
555}
556impl Betanumeric for char {
557    fn is_betanumeric(&self) -> bool {
558        BETANUMERIC_DIGITS.contains(*self)
559    }
560    fn to_betanumeric_ordinal(&self) -> Option<usize> {
561        BETANUMERIC_DIGITS.chars().position(|x| x.eq(self))
562    }
563}
564impl<T: AsRef<str>> PersistentIdentifierConvert<T> for T
565where
566    T: ToString,
567{
568    fn format_as(&self, pid_type: PID) -> String {
569        match pid_type {
570            | PID::ARK => ARK::format(self.as_ref()),
571            | PID::Arxiv => Arxiv::format(self.as_ref()),
572            | PID::DOI => DOI::format(self.as_ref()),
573            | PID::Handle => Handle::format(self.as_ref()),
574            | PID::ISBN => ISBN::format(self.as_ref()),
575            | PID::ISNI => ISNI::format(self.as_ref()),
576            | PID::ORCID => ORCID::format(self.as_ref()),
577            | PID::Patent => Patent::format(self.as_ref()),
578            | PID::RAID => RAID::format(self.as_ref()),
579            | PID::ROR => <ROR as PersistentIdentifierParse>::format(self.as_ref()),
580            | PID::SWHID => SWHID::format(self.as_ref()),
581            | _ => self.as_ref().to_string(),
582        }
583    }
584    fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal {
585        let value = self.as_ref().to_string();
586        match pid_type {
587            | PID::ARK => PersistentIdentifierInternal { value, pid_type: PID::ARK },
588            | PID::Arxiv => PersistentIdentifierInternal { value, pid_type: PID::Arxiv },
589            | PID::DOI => PersistentIdentifierInternal { value, pid_type: PID::DOI },
590            | PID::Handle => PersistentIdentifierInternal {
591                value,
592                pid_type: PID::Handle,
593            },
594            | PID::ISBN => PersistentIdentifierInternal { value, pid_type: PID::ISBN },
595            | PID::ISNI => PersistentIdentifierInternal { value, pid_type: PID::ISNI },
596            | PID::ORCID => PersistentIdentifierInternal { value, pid_type: PID::ORCID },
597            | PID::Patent => PersistentIdentifierInternal {
598                value,
599                pid_type: PID::Patent,
600            },
601            | PID::RAID => PersistentIdentifierInternal { value, pid_type: PID::RAID },
602            | PID::ROR => PersistentIdentifierInternal { value, pid_type: PID::ROR },
603            | PID::SWHID => PersistentIdentifierInternal { value, pid_type: PID::SWHID },
604            | _ => PersistentIdentifierInternal::default(),
605        }
606    }
607    fn is_pid(&self, pid_type: PID) -> bool {
608        match pid_type {
609            | PID::ARK => self.is_ark(),
610            | PID::Arxiv => self.is_arxiv(),
611            | PID::DOI => self.is_doi(),
612            | PID::Handle => self.is_handle(),
613            | PID::ISBN => self.is_isbn(),
614            | PID::ISNI => self.is_isni(),
615            | PID::ORCID => self.is_orcid(),
616            | PID::Patent => Patent::is_valid(self.as_ref()),
617            | PID::RAID => self.is_raid(),
618            | PID::ROR => self.is_ror(),
619            | PID::SWHID => self.is_swhid(),
620            | _ => false,
621        }
622    }
623    fn is_ark(&self) -> bool {
624        ARK::is_valid(self.as_ref())
625    }
626    fn is_arxiv(&self) -> bool {
627        Arxiv::is_valid(self.as_ref())
628    }
629    fn is_doi(&self) -> bool {
630        DOI::is_valid(self.as_ref())
631    }
632    fn is_handle(&self) -> bool {
633        Handle::is_valid(self.as_ref())
634    }
635    fn is_isbn(&self) -> bool {
636        ISBN::is_valid(self.as_ref())
637    }
638    fn is_isni(&self) -> bool {
639        ISNI::is_valid(self.as_ref())
640    }
641    fn is_orcid(&self) -> bool {
642        ORCID::is_valid(self.as_ref())
643    }
644    fn is_raid(&self) -> bool {
645        RAID::is_valid(self.as_ref())
646    }
647    fn is_ror(&self) -> bool {
648        ROR::is_valid(self.as_ref())
649    }
650    fn is_swhid(&self) -> bool {
651        SWHID::is_valid(self.as_ref())
652    }
653}
654impl PersistentIdentifierInternal {
655    /// Convert a `PersistentIdentifierInternal` to an `ARK`
656    pub fn to_ark(&self) -> ARK {
657        let PersistentIdentifierInternal { value, pid_type } = self;
658        match pid_type {
659            | PID::ARK => ARK::from_string(value),
660            | _ => ARK::default(),
661        }
662    }
663    /// Convert a `PersistentIdentifierInternal` to an [`Arxiv`].
664    pub fn to_arxiv(&self) -> Arxiv {
665        let PersistentIdentifierInternal { value, pid_type } = self;
666        match pid_type {
667            | PID::Arxiv => Arxiv::from_string(value),
668            | _ => Arxiv::default(),
669        }
670    }
671    /// Convert a `PersistentIdentifierInternal` to a `DOI`
672    pub fn to_doi(&self) -> DOI {
673        let PersistentIdentifierInternal { value, pid_type } = self;
674        match pid_type {
675            | PID::DOI => DOI::from_string(value),
676            | _ => DOI::default(),
677        }
678    }
679    /// Convert a `PersistentIdentifierInternal` to a [`Handle`].
680    pub fn to_handle(&self) -> Handle {
681        let PersistentIdentifierInternal { value, pid_type } = self;
682        match pid_type {
683            | PID::Handle => Handle::from_string(value),
684            | _ => Handle::default(),
685        }
686    }
687    /// Convert a `PersistentIdentifierInternal` to an `ISBN`
688    pub fn to_isbn(&self) -> ISBN {
689        let PersistentIdentifierInternal { value, pid_type } = self;
690        match pid_type {
691            | PID::ISBN => ISBN::from_string(value),
692            | _ => ISBN::default(),
693        }
694    }
695    /// Convert a `PersistentIdentifierInternal` to a `ORCID`
696    pub fn to_orcid(&self) -> ORCID {
697        let PersistentIdentifierInternal { value, pid_type } = self;
698        match pid_type {
699            | PID::ORCID => ORCID::from_string(value),
700            | _ => ORCID::default(),
701        }
702    }
703    /// Convert a `PersistentIdentifierInternal` to an `ISNI`
704    pub fn to_isni(&self) -> ISNI {
705        let PersistentIdentifierInternal { value, pid_type } = self;
706        match pid_type {
707            | PID::ISNI => ISNI::from_string(value),
708            | _ => ISNI::default(),
709        }
710    }
711    /// Convert a `PersistentIdentifierInternal` to a [`Patent`].
712    pub fn to_patent(&self) -> Patent {
713        let PersistentIdentifierInternal { value, pid_type } = self;
714        match pid_type {
715            | PID::Patent => Patent::from_string(value),
716            | _ => Patent::default(),
717        }
718    }
719    /// Convert a `PersistentIdentifierInternal` to a `RAID`
720    pub fn to_raid(&self) -> RAID {
721        let PersistentIdentifierInternal { value, pid_type } = self;
722        match pid_type {
723            | PID::RAID => RAID::from_string(value),
724            | _ => RAID::default(),
725        }
726    }
727    /// Convert a `PersistentIdentifierInternal` to a `ROR`
728    pub fn to_ror(&self) -> ROR {
729        let PersistentIdentifierInternal { value, pid_type } = self;
730        match pid_type {
731            | PID::ROR => ROR::from_string(value),
732            | _ => ROR::default(),
733        }
734    }
735    /// Convert a `PersistentIdentifierInternal` to a `SWHID`.
736    pub fn to_swhid(&self) -> SWHID {
737        let PersistentIdentifierInternal { value, pid_type } = self;
738        match pid_type {
739            | PID::SWHID => SWHID::from_string(value),
740            | _ => SWHID::default(),
741        }
742    }
743}
744impl From<&str> for PublicationIdentifierType {
745    fn from(value: &str) -> Self {
746        let match_covers_value = RE_ARXIV
747            .find(value)
748            .ok()
749            .flatten()
750            .is_some_and(|matched| matched.start() == 0 && matched.end() == value.len());
751        let explicitly_labeled_arxiv = match_covers_value && Arxiv::is_valid(value);
752        match (DOI::is_valid(value), explicitly_labeled_arxiv) {
753            | (true, _) => Self::Doi(DOI::from_string(value)),
754            | (_, true) => Self::Arxiv(Arxiv::from_string(value)),
755            | _ => Self::Unknown,
756        }
757    }
758}
759/// Calculates the check character for an ISBN-10 or ISBN-13 value.
760///
761/// Hyphens and spaces are removed before selecting the algorithm from the compact
762/// length. ISBN-10 weights the first nine digits from 10 through 2 and applies
763/// modulo 11, representing a result of 10 as `X`. ISBN-13 weights the first
764/// twelve digits alternately by 1 and 3 and applies modulo 10.
765///
766/// Returns `None` when the compact value is not 10 or 13 characters long, when a
767/// payload character is not decimal, or when checked arithmetic fails.
768///
769/// ## Examples
770/// ```rust
771/// use acorn::schema::pid::{ISBN, PersistentIdentifier, PersistentIdentifierParse};
772///
773/// let isbn_10 = ISBN::from_string("0-8044-2957-X");
774/// let isbn_13 = ISBN::from_string("978-0-306-40627-0");
775/// assert_eq!(isbn_10.check_digit(), Some(vec!['X']));
776/// assert_eq!(isbn_13.check_digit(), Some(vec!['0']));
777/// ```
778fn mod_10_or_11_check_digit<S>(value: S) -> Option<Vec<char>>
779where
780    S: AsRef<str>,
781{
782    let working = value
783        .as_ref()
784        .chars()
785        .filter(|character| !matches!(character, '-' | ' '))
786        .collect::<String>();
787    match working.len() {
788        | 10 => working
789            .chars()
790            .take(9)
791            .enumerate()
792            .try_fold(0_u32, |sum, (index, character)| {
793                character.to_digit(10).and_then(|digit| {
794                    u32::try_from(index)
795                        .ok()
796                        .and_then(|index| 10_u32.checked_sub(index))
797                        .and_then(|weight| digit.checked_mul(weight))
798                        .and_then(|weighted| sum.checked_add(weighted))
799                })
800            })
801            .and_then(|sum| sum.checked_rem(11))
802            .and_then(|remainder| 11_u32.checked_sub(remainder))
803            .and_then(|complement| complement.checked_rem(11))
804            .and_then(|check_digit| match check_digit {
805                | 10 => Some(vec!['X']),
806                | value => char::from_digit(value, 10).map(|character| vec![character]),
807            }),
808        | 13 => working
809            .chars()
810            .take(12)
811            .enumerate()
812            .try_fold(0_u32, |sum, (index, character)| {
813                character.to_digit(10).and_then(|digit| {
814                    index
815                        .checked_rem(2)
816                        .map(|remainder| if remainder == 0 { 1 } else { 3 })
817                        .and_then(|weight| digit.checked_mul(weight))
818                        .and_then(|weighted| sum.checked_add(weighted))
819                })
820            })
821            .and_then(|sum| sum.checked_rem(10))
822            .and_then(|remainder| 10_u32.checked_sub(remainder))
823            .and_then(|complement| complement.checked_rem(10))
824            .and_then(|check_digit| char::from_digit(check_digit, 10).map(|character| vec![character])),
825        | _ => None,
826    }
827}
828/// Calculates an ISO 7064 MOD 11-2 check character for an ORCID or ISNI value.
829///
830/// Hyphens and spaces are removed, after which the first 15 characters are folded
831/// through `(remainder + digit) * 2 mod 11`. The final complement is returned as
832/// one decimal character, or as `X` when the result is 10. A non-decimal payload
833/// character contributes zero; PID parsing is responsible for enforcing the
834/// identifier's character set before validation.
835///
836/// Returns `None` only when checked arithmetic or character conversion fails.
837///
838/// ## Examples
839/// ```rust
840/// use acorn::schema::pid::{ISNI, ORCID, PersistentIdentifier, PersistentIdentifierParse};
841///
842/// let isni = ISNI::from_string("0000000492299539");
843/// let orcid = ORCID::from_string("0000-0002-2816-415X");
844/// assert_eq!(isni.check_digit(), Some(vec!['9']));
845/// assert_eq!(orcid.check_digit(), Some(vec!['X']));
846/// ```
847fn mod_11_2_check_digit<S>(value: S) -> Option<Vec<char>>
848where
849    S: AsRef<str>,
850{
851    const COMPLEMENT: u32 = 12;
852    const MODULUS: u32 = 11;
853    const RADIX: u32 = 2;
854    let working = value.as_ref().replace("-", "").replace(" ", "");
855    let remainder = working.chars().take(15).try_fold(0_u32, |remainder, value| {
856        let digit = value.to_digit(10).unwrap_or_default();
857        remainder
858            .checked_add(digit)
859            .and_then(|sum| sum.checked_mul(RADIX))
860            .and_then(|product| product.checked_rem(MODULUS))
861    });
862    remainder
863        .and_then(|remainder| COMPLEMENT.checked_sub(remainder))
864        .and_then(|value| value.checked_rem(MODULUS))
865        .and_then(|result| match result {
866            | 10 => Some(vec!['X']),
867            | value => char::from_digit(value, 10).map(|value| vec![value]),
868        })
869}
870/// Calculates the two-character MOD 97-10 checksum used by a ROR identifier.
871///
872/// Hyphens and spaces are removed before the first six Crockford Base32
873/// characters are decoded. The decoded value is multiplied by 100, reduced
874/// modulo 97, and complemented; the result is returned as two zero-padded decimal
875/// characters.
876///
877/// Returns `None` when the Base32 payload cannot be decoded or checked arithmetic
878/// fails.
879///
880/// ## Example
881/// ```rust
882/// use acorn::schema::pid::{PersistentIdentifier, PersistentIdentifierParse, ROR};
883///
884/// let ror = ROR::from_string("01qz5mb56");
885/// assert_eq!(ror.check_digit(), Some(vec!['5', '6']));
886/// ```
887fn mod_97_10_check_digit<S>(value: S) -> Option<Vec<char>>
888where
889    S: AsRef<str>,
890{
891    const COMPLEMENT: u128 = 98;
892    const MODULUS: u128 = 97;
893    let working = value
894        .as_ref()
895        .replace("-", "")
896        .replace(" ", "")
897        .chars()
898        .take(6)
899        .map(String::from)
900        .collect::<Vec<_>>()
901        .join("");
902    base32_crockford_decode(working)
903        .and_then(|value| value.checked_mul(100))
904        .and_then(|value| value.checked_rem(MODULUS))
905        .and_then(|remainder| COMPLEMENT.checked_sub(remainder))
906        .and_then(|complement| complement.checked_rem(MODULUS))
907        .map(|checksum| format!("{checksum:02}").chars().collect())
908}
909/// Calculates a NOID check character using the NCDA beta-numeric alphabet.
910///
911/// Each character's beta-numeric ordinal is multiplied by its one-based position.
912/// The running total is reduced modulo 29, and the resulting ordinal is converted
913/// back into a single beta-numeric character. Characters outside the alphabet
914/// contribute an ordinal of zero.
915///
916/// Returns `None` when checked arithmetic or ordinal conversion fails.
917///
918/// ## Example
919/// ```rust
920/// use acorn::schema::pid::noid_check_digit;
921///
922/// assert_eq!(noid_check_digit("13030/xf93gt2"), Some(vec!['q']));
923/// ```
924pub fn noid_check_digit<S>(value: S) -> Option<Vec<char>>
925where
926    S: AsRef<str>,
927{
928    const RADIX: usize = 29;
929    let remainder = value.as_ref().chars().enumerate().try_fold(0_usize, |acc, (index, value)| {
930        let ordinal = value.to_betanumeric_ordinal().unwrap_or(0);
931        index
932            .checked_rem(RADIX)
933            .and_then(|position| position.checked_add(1))
934            .and_then(|position| position.checked_mul(ordinal))
935            .and_then(|weighted| acc.checked_add(weighted))
936            .and_then(|sum| sum.checked_rem(RADIX))
937    });
938    remainder
939        .and_then(|value| u8::try_from(value).ok())
940        .and_then(to_betanumeric)
941        .map(|value| vec![value])
942}
943fn to_betanumeric(value: u8) -> Option<char> {
944    match BETANUMERIC_DIGITS.chars().enumerate().find(|(i, _)| *i == value as usize) {
945        | Some((_, x)) => Some(x),
946        | None => None,
947    }
948}
949
950#[cfg(test)]
951mod tests;