1use 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
49pub trait Betanumeric {
53 fn is_betanumeric(&self) -> bool {
55 false
56 }
57 fn to_betanumeric_ordinal(&self) -> Option<usize>;
61}
62pub trait PersistentIdentifier: fmt::Display {
64 fn new() -> Self;
66 fn schema_uri(&self) -> String;
71 fn identifier(&self) -> String;
77 fn prefix(&self) -> Option<String> {
81 None
82 }
83 fn suffix(&self) -> Option<String>;
87 fn check_digit(&self) -> Option<Vec<char>> {
89 None
90 }
91 fn url(&self) -> String {
93 String::new()
94 }
95}
96pub trait PersistentIdentifierConvert<T: AsRef<str>> {
98 fn format_as(&self, pid_type: PID) -> String;
106 fn to_pid(&self, pid_type: PID) -> PersistentIdentifierInternal;
114 fn is_pid(&self, _pid_type: PID) -> bool;
121 fn is_ark(&self) -> bool;
128 fn is_arxiv(&self) -> bool;
130 fn is_doi(&self) -> bool;
137 fn is_handle(&self) -> bool;
139 fn is_isbn(&self) -> bool {
141 false
142 }
143 fn is_isni(&self) -> bool;
145 fn is_orcid(&self) -> bool;
152 fn is_raid(&self) -> bool;
159 fn is_ror(&self) -> bool;
166 fn is_swhid(&self) -> bool;
168}
169pub trait PersistentIdentifierParse {
171 fn find_all(value: impl ToString) -> Vec<Self>
173 where
174 Self: Sized;
175 fn format(value: impl ToString) -> String;
177 fn from_string(value: impl ToString) -> Self
179 where
180 Self: Sized;
181 fn is_valid(value: impl ToString) -> bool;
183}
184#[derive(Clone, Debug, Default, EnumIs, EnumIter, Eq, Ord, PartialEq, PartialOrd)]
188pub enum PID {
189 #[default]
191 Unknown,
192 ARK,
198 Arxiv,
202 DOI,
206 Handle,
210 ISBN,
214 ISNI,
218 ORCID,
222 Patent,
224 PIDINST,
230 RAID,
238 ROR,
244 SWHID,
248 URL,
250}
251#[derive(Clone, Debug)]
253pub enum PublicationIdentifierType {
254 Doi(DOI),
256 Arxiv(Arxiv),
258 Unknown,
260}
261#[derive(Builder, Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
263#[builder(start_fn = init, on(String, into))]
264pub struct Identifier {
265 pub kind: PID,
267 pub value: String,
269}
270#[derive(Default)]
272pub struct PersistentIdentifierInternal {
273 value: String,
275 pid_type: PID,
277}
278impl Identifier {
279 pub fn new(value: impl Into<String>) -> Self {
281 Self {
282 kind: PID::Unknown,
283 value: value.into(),
284 }
285 }
286 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}
759fn 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}
828fn 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}
870fn 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}
909pub 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;