use crate::TaxonomicRank;
use std::sync::OnceLock;
include!("taxonomy_paths.rs");
include!("taxonomy_species.rs");
#[path = "taxonomy_species_impl.rs"]
mod species_impl;
pub const SOURCE_VERSION: &str = "2026-09-11";
pub const SOURCE_DOI: &str = "https://doi.org/10.48580/dgz5n";
pub const SOURCE_DATASET: u32 = 316_321;
pub const MANIFEST: &str = include_str!("../data/taxonomy/manifest.json");
pub const NOTICE: &str = include_str!("../data/taxonomy/NOTICE.md");
#[must_use]
pub fn snapshot_tsv() -> &'static str {
TSV
}
const TSV: &str = include_str!("../data/taxonomy/taxa.tsv");
#[derive(Debug)]
struct Record {
id: &'static str,
name: &'static str,
rank: &'static str,
parent: Option<usize>,
label: Option<&'static str>,
wikidata: Option<&'static str>,
wikipedia: Option<&'static str>,
extinct: Option<bool>,
source_id: Option<&'static str>,
}
struct Index {
records: Vec<Record>,
children: Vec<Vec<usize>>,
}
fn nonempty(value: &'static str) -> Option<&'static str> {
(!value.is_empty()).then_some(value)
}
fn index() -> &'static Index {
static INDEX: OnceLock<Index> = OnceLock::new();
INDEX.get_or_init(|| {
let rows: Vec<Vec<&str>> = TSV
.lines()
.skip(1)
.map(|row| row.split('\t').collect())
.collect();
let records: Vec<Record> = rows
.iter()
.map(|row| Record {
id: row[0],
source_id: nonempty(row[8]),
name: row[2],
rank: row[3],
parent: nonempty(row[1]).map(|id| {
rows.binary_search_by_key(&id, |r| r[0])
.expect("bundled taxonomy parent")
}),
extinct: match row[4] {
"true" => Some(true),
"false" => Some(false),
_ => None,
},
label: nonempty(row[5]),
wikidata: nonempty(row[6]),
wikipedia: nonempty(row[7]),
})
.collect();
let mut children = vec![Vec::new(); records.len()];
for (i, record) in records.iter().enumerate() {
if let Some(parent) = record.parent {
children[parent].push(i);
}
}
for siblings in &mut children {
siblings.sort_unstable_by_key(|&i| (records[i].name, records[i].id));
}
Index { records, children }
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Taxon(usize);
impl Taxon {
fn record(self) -> &'static Record {
&index().records[self.0]
}
pub fn all() -> impl ExactSizeIterator<Item = Self> + DoubleEndedIterator {
(0..index().records.len()).map(Self)
}
#[must_use]
pub fn count() -> usize {
index().records.len()
}
#[must_use]
pub fn by_id(id: &str) -> Option<Self> {
index()
.records
.binary_search_by_key(&id, |r| r.id)
.ok()
.map(Self)
}
pub fn named(name: &str) -> impl Iterator<Item = Self> + '_ {
Self::all().filter(move |taxon| {
taxon.scientific_name().eq_ignore_ascii_case(name)
|| taxon
.english_label()
.is_some_and(|label| label.eq_ignore_ascii_case(name))
})
}
pub fn roots() -> impl Iterator<Item = Self> {
Self::all().filter(|taxon| taxon.record().parent.is_none())
}
#[must_use]
pub fn id(self) -> &'static str {
self.record().id
}
#[must_use]
pub fn scientific_name(self) -> &'static str {
self.record().name
}
#[must_use]
pub fn source_rank(self) -> &'static str {
self.record().rank
}
#[must_use]
pub fn rank(self) -> Option<TaxonomicRank> {
match self.source_rank() {
"domain" => Some(TaxonomicRank::Domain),
"kingdom" => Some(TaxonomicRank::Kingdom),
"phylum" => Some(TaxonomicRank::Phylum),
"class" => Some(TaxonomicRank::Class),
"order" => Some(TaxonomicRank::Order),
"family" => Some(TaxonomicRank::Family),
"genus" => Some(TaxonomicRank::Genus),
"species" => Some(TaxonomicRank::Species),
_ => None,
}
}
#[must_use]
pub fn english_label(self) -> Option<&'static str> {
self.record().label
}
#[must_use]
pub fn wikidata_id(self) -> Option<&'static str> {
self.record().wikidata
}
#[must_use]
pub fn wikipedia_url(self) -> Option<&'static str> {
self.record().wikipedia
}
#[must_use]
pub fn is_selected_species(self) -> bool {
self.wikidata_id().is_some()
}
#[must_use]
pub fn species(self) -> Option<Species> {
Species::from_taxon(self)
}
#[must_use]
pub fn extinct(self) -> Option<bool> {
self.record().extinct
}
#[must_use]
pub fn source_dataset_id(self) -> Option<&'static str> {
self.record().source_id
}
#[must_use]
pub fn parent(self) -> Option<Self> {
self.record().parent.map(Self)
}
pub fn children(self) -> impl ExactSizeIterator<Item = Self> + DoubleEndedIterator {
index().children[self.0].iter().copied().map(Self)
}
pub fn ancestors(self) -> impl Iterator<Item = Self> {
std::iter::successors(self.parent(), |taxon| taxon.parent())
}
#[must_use]
pub fn lineage(self) -> Vec<Self> {
let mut path = vec![self];
path.extend(self.ancestors());
path.reverse();
path
}
}
impl std::fmt::Display for Taxon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.scientific_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TaxonKey {
pub source_version: String,
pub col_id: String,
}
impl TaxonKey {
#[must_use]
pub fn resolve(&self) -> Option<Taxon> {
if self.source_version == SOURCE_VERSION {
Taxon::by_id(&self.col_id)
} else {
None
}
}
}
impl Taxon {
#[must_use]
pub fn key(self) -> TaxonKey {
TaxonKey {
source_version: SOURCE_VERSION.to_owned(),
col_id: self.id().to_owned(),
}
}
}