Skip to main content

music21/metadata/
mod.rs

1//! A score's metadata: music21's `metadata.Metadata`.
2//!
3//! music21 keeps metadata as named values, each name a *unique name* from a
4//! fixed vocabulary drawn from Dublin Core, the MARC relator terms and
5//! Humdrum's reference records, or one a caller made up. A name may carry
6//! several values, and the names keep the order they were first given in,
7//! which is the order a MusicXML writer puts them out in.
8//!
9//! A contributor is a value with a role: the composer, an arranger, a
10//! lyricist. Its unique name is its role where the role is in the
11//! vocabulary, and `otherContributor` where it is not.
12
13pub mod primitives;
14
15use std::borrow::Cow;
16
17/// What a metadata value says beyond its text and its role: one of
18/// music21's primitives.
19#[derive(Clone, Debug, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum MetadataContent {
22    /// A piece of text with the language it is written in: music21's
23    /// `Text`, and its `Copyright` where the text carries a role.
24    Text(primitives::Text),
25    /// Somebody who had a hand in the work: music21's `Contributor`, which
26    /// may go under several names and may say when they lived.
27    Contributor(primitives::Contributor),
28    /// A date, which may be approximate, uncertain, a bound, a span or a
29    /// choice: music21's `DatePrimitive` and the four classes under it.
30    Date(primitives::DatePrimitive),
31}
32
33/// One value of a metadata name: its text and, for a contributor or a
34/// copyright, the role it was given -- and the primitive it is, which says
35/// what more is known about it.
36///
37/// **The text and the role are a reading of the content, not a second copy
38/// of it.** Every way of building one of these fills all three together and
39/// nothing can write to them afterwards, so they cannot drift apart: a
40/// contributor's text is the first name they go by, and a date's is the way
41/// music21 writes it.
42///
43/// A value built as a plain text is not equal to one built as a contributor
44/// or a date of the same text, and that is right: the one knows what it is
45/// and the other does not.
46#[derive(Clone, Debug, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct MetadataValue {
49    text: String,
50    role: Option<String>,
51    content: MetadataContent,
52}
53
54impl MetadataValue {
55    /// A value that is only text.
56    pub fn new(text: impl Into<String>) -> Self {
57        Self::of_text(primitives::Text::new(text))
58    }
59
60    /// A value with a role, as a contributor or a copyright has.
61    pub fn with_role(text: impl Into<String>, role: impl Into<String>) -> Self {
62        Self::of_text(primitives::Text::new(text).in_role(role))
63    }
64
65    /// A value that is a piece of text saying what language it is in:
66    /// music21's `Text`, or its `Copyright` where the text has a role.
67    pub fn of_text(text: primitives::Text) -> Self {
68        Self {
69            text: text.data().to_string(),
70            role: text.role().map(str::to_string),
71            content: MetadataContent::Text(text),
72        }
73    }
74
75    /// A value that is somebody who had a hand in the work: music21's
76    /// `Contributor`. Its text is the first name they go by, which is what
77    /// music21's own writers write.
78    pub fn of_contributor(contributor: primitives::Contributor) -> Self {
79        Self {
80            text: contributor
81                .name()
82                .map(primitives::Text::data)
83                .unwrap_or_default()
84                .to_string(),
85            role: contributor.role().map(str::to_string),
86            content: MetadataContent::Contributor(contributor),
87        }
88    }
89
90    /// A value that is a date: music21's four date classes. Its text is
91    /// the way music21 writes that date.
92    pub fn of_date(date: primitives::DatePrimitive) -> Self {
93        Self {
94            text: date.to_string(),
95            role: None,
96            content: MetadataContent::Date(date),
97        }
98    }
99
100    /// The value as text: music21's `str(value)`.
101    pub fn text(&self) -> &str {
102        &self.text
103    }
104
105    /// The role, where the value has one.
106    pub fn role(&self) -> Option<&str> {
107        self.role.as_deref()
108    }
109
110    /// What the value is: a text, a contributor or a date.
111    pub fn content(&self) -> &MetadataContent {
112        &self.content
113    }
114
115    /// The text with its language, where the value is one.
116    pub fn as_text(&self) -> Option<&primitives::Text> {
117        match &self.content {
118            MetadataContent::Text(text) => Some(text),
119            _ => None,
120        }
121    }
122
123    /// The contributor, where the value is one.
124    pub fn as_contributor(&self) -> Option<&primitives::Contributor> {
125        match &self.content {
126            MetadataContent::Contributor(contributor) => Some(contributor),
127            _ => None,
128        }
129    }
130
131    /// The date, where the value is one.
132    pub fn as_date(&self) -> Option<&primitives::DatePrimitive> {
133        match &self.content {
134            MetadataContent::Date(date) => Some(date),
135            _ => None,
136        }
137    }
138}
139
140/// A score's metadata: named values in the order the names were first given.
141///
142/// ```
143/// use music21::metadata::Metadata;
144///
145/// let mut md = Metadata::new();
146/// md.add_text("title", "Wer nur den lieben Gott");
147/// md.add_contributor("composer", "J. S. Bach");
148/// assert_eq!(md.first_text("title"), Some("Wer nur den lieben Gott"));
149/// assert_eq!(md.contributors().next().map(|(_, value)| value.text()), Some("J. S. Bach"));
150/// ```
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153#[must_use]
154pub struct Metadata {
155    entries: Vec<(String, Vec<MetadataValue>)>,
156}
157
158impl Metadata {
159    /// Metadata with nothing in it.
160    ///
161    /// music21's own constructor starts with its version under `software`;
162    /// this one starts empty, and a caller who wants a software line adds it.
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Adds a value under a unique name, after any it already has.
168    pub fn add(&mut self, unique_name: impl Into<String>, value: MetadataValue) {
169        let unique_name = unique_name.into();
170        match self
171            .entries
172            .iter_mut()
173            .find(|(name, _)| *name == unique_name)
174        {
175            Some((_, values)) => values.push(value),
176            None => self.entries.push((unique_name, vec![value])),
177        }
178    }
179
180    /// Adds a value that is only text.
181    pub fn add_text(&mut self, unique_name: impl Into<String>, text: impl Into<String>) {
182        self.add(unique_name, MetadataValue::new(text));
183    }
184
185    /// Adds a contributor in a role: under the role itself where it is one
186    /// of the vocabulary's contributor names, and under `otherContributor`
187    /// where it is not, as music21's `addContributor` files one.
188    pub fn add_contributor(&mut self, role: impl Into<String>, name: impl Into<String>) {
189        let role = role.into();
190        let unique_name = if is_contributor_unique_name(&role) {
191            role.clone()
192        } else {
193            "otherContributor".to_string()
194        };
195        self.add(unique_name, MetadataValue::with_role(name, role));
196    }
197
198    /// Replaces every value under a unique name.
199    pub fn set(&mut self, unique_name: impl Into<String>, values: Vec<MetadataValue>) {
200        let unique_name = unique_name.into();
201        self.entries.retain(|(name, _)| *name != unique_name);
202        if !values.is_empty() {
203            self.entries.push((unique_name, values));
204        }
205    }
206
207    /// Every value under a unique name, in the order they were added.
208    pub fn get(&self, unique_name: &str) -> &[MetadataValue] {
209        self.entries
210            .iter()
211            .find(|(name, _)| name == unique_name)
212            .map_or(&[], |(_, values)| values.as_slice())
213    }
214
215    /// The text of the first value under a unique name.
216    pub fn first_text(&self, unique_name: &str) -> Option<&str> {
217        self.get(unique_name).first().map(MetadataValue::text)
218    }
219
220    /// Every name and value in order, names in the order they were first
221    /// given and each name's values together: music21's `all` with
222    /// `returnSorted=False`.
223    pub fn all(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
224        self.entries
225            .iter()
226            .flat_map(|(name, values)| values.iter().map(move |value| (name.as_str(), value)))
227    }
228
229    /// The contributors alone, in the same order.
230    pub fn contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
231        self.all()
232            .filter(|(name, _)| is_contributor_unique_name(name))
233    }
234
235    /// Every value under a unique name as one piece of text, as music21's
236    /// attribute of that name reads it -- `title`, `composer`, `movementName`:
237    /// one contributor by name, two joined with `and`, more as the first
238    /// `and` how many others; a title's values each with an article written
239    /// after a comma put back in front; anything else the values joined with
240    /// commas. Nothing for a name outside the vocabulary, or with no value.
241    pub fn string_value(&self, unique_name: &str) -> Option<String> {
242        namespace_name(unique_name)?;
243        let values = self.get(unique_name);
244        if values.is_empty() {
245            return None;
246        }
247        if is_contributor_unique_name(unique_name) {
248            return Some(match values {
249                [only] => only.text().to_string(),
250                [first, second] => format!("{} and {}", first.text(), second.text()),
251                [first, rest @ ..] => format!("{} and {} others", first.text(), rest.len()),
252                [] => String::new(),
253            });
254        }
255        let texts: Vec<String> = if ARTICLED_TITLES.contains(&unique_name) {
256            values
257                .iter()
258                .map(|value| {
259                    crate::text::prepend_article(value.text(), None)
260                        .unwrap_or_else(|_| value.text().to_string())
261                })
262                .collect()
263        } else {
264            values
265                .iter()
266                .map(|value| value.text().to_string())
267                .collect()
268        };
269        Some(texts.join(", "))
270    }
271
272    /// The title, as [`Self::string_value`] reads it: music21's `title`.
273    pub fn title(&self) -> Option<String> {
274        self.string_value("title")
275    }
276
277    /// The title, or the next best thing the score says: music21's
278    /// `bestTitle`, which takes the first of `title`, `popularTitle`,
279    /// `alternativeTitle` and `movementName` that says anything.
280    ///
281    /// ```
282    /// use music21::metadata::Metadata;
283    ///
284    /// let mut said = Metadata::new();
285    /// said.add_text("popularTitle", "Eroica");
286    /// assert_eq!(said.best_title().as_deref(), Some("Eroica"));
287    /// said.add_text("title", "Third Symphony");
288    /// assert_eq!(said.best_title().as_deref(), Some("Third Symphony"));
289    /// assert_eq!(Metadata::new().best_title(), None);
290    /// ```
291    pub fn best_title(&self) -> Option<String> {
292        ["title", "popularTitle", "alternativeTitle", "movementName"]
293            .into_iter()
294            .find_map(|name| self.string_value(name).filter(|text| !text.is_empty()))
295    }
296
297    /// The contributors in a role, or the ones with no role of their own
298    /// where none is asked for: music21's `getContributorsByRole`, which a
299    /// MusicXML file needs because some of them name a contributor and no
300    /// role.
301    pub fn contributors_by_role(&self, role: Option<&str>) -> Vec<&MetadataValue> {
302        self.contributors()
303            .filter(|(_, value)| value.role() == role)
304            .map(|(_, value)| value)
305            .collect()
306    }
307
308    /// Whether nothing is in it.
309    pub fn is_empty(&self) -> bool {
310        self.entries.is_empty()
311    }
312
313    /// Every name and value sorted by name and then by text, as music21's
314    /// `all` reads them by default and [`Self::search`] walks them.
315    pub fn all_sorted(&self) -> Vec<(&str, &MetadataValue)> {
316        let mut all: Vec<(&str, &MetadataValue)> = self.all().collect();
317        all.sort_by(|(left, left_value), (right, right_value)| {
318            left.cmp(right)
319                .then_with(|| left_value.text().cmp(right_value.text()))
320        });
321        all
322    }
323
324    /// The first value a query matches, and the field it was found under:
325    /// music21's `search`.
326    ///
327    /// With no `field`, every standard name is searched, sorted by name,
328    /// and then every contributor in the order they were added, under its
329    /// role rather than its unique name so that a made-up role can be
330    /// searched for.
331    ///
332    /// A `field` names where to look, and need not be the whole name:
333    /// music21 takes a unique name, one of its four grandfathered work ids
334    /// (`date`, `volume`, `dedication`, `commission`), one of its twenty
335    /// Humdrum abbreviations (`otl`, `ode`), or any piece of a name --
336    /// `compos` finds `composer`. A field naming nothing finds nothing.
337    ///
338    /// ```
339    /// use music21::metadata::{Metadata, MetadataQuery};
340    ///
341    /// let mut md = Metadata::new();
342    /// md.add_contributor("composer", "Joplin, Scott");
343    /// md.add_text("title", "Maple Leaf Rag");
344    ///
345    /// let joplin = MetadataQuery::Text("joplin");
346    /// assert_eq!(
347    ///     md.search(&joplin, Some("composer")).unwrap().field(),
348    ///     Some("composer")
349    /// );
350    /// assert_eq!(
351    ///     md.search(&joplin, Some("compos")).unwrap().field(),
352    ///     Some("composer")
353    /// );
354    /// assert!(md.search(&joplin, Some("title")).is_none());
355    ///
356    /// let found = md.search(&MetadataQuery::Text("leaf"), None).unwrap();
357    /// assert_eq!((found.field(), found.text()), (Some("title"), "Maple Leaf Rag"));
358    /// ```
359    pub fn search(
360        &self,
361        query: &MetadataQuery<'_>,
362        field: Option<&str>,
363    ) -> Option<MetadataMatch<'_>> {
364        let field = field.map(str::to_lowercase);
365        self.search_fields(field.as_deref())
366            .into_iter()
367            .find(|(text, _)| query.matches(text))
368            .map(|(text, field)| MetadataMatch { field, text })
369    }
370
371    /// The text a search looks through and the field each piece answers
372    /// under, in the order music21 builds them.
373    fn search_fields(&self, field: Option<&str>) -> Vec<(Cow<'_, str>, Option<&str>)> {
374        let mut pairs: Vec<(Cow<'_, str>, Option<&str>)> = Vec::new();
375        match field {
376            Some(field) => {
377                let named = unique_name_of_field(field);
378                let texts =
379                    named.map_or_else(Vec::new, |(unique_name, _)| self.field_texts(unique_name));
380                match (texts.is_empty(), named) {
381                    // music21 answers under the field as it was asked for,
382                    // not under the unique name that field stands for.
383                    (false, Some((_, asked))) => {
384                        pairs.extend(texts.into_iter().map(|text| (text, Some(asked))));
385                    }
386                    _ => pairs.extend(self.partial_field_match(field)),
387                }
388            }
389            None => pairs.extend(
390                self.standard_non_contributors()
391                    .map(|(name, value)| (Cow::Borrowed(value.text()), Some(name))),
392            ),
393        }
394        pairs.extend(
395            self.contributors()
396                .filter(|(_, value)| contributor_answers_to(value.role(), field))
397                .map(|(_, value)| (Cow::Borrowed(value.text()), value.role())),
398        );
399        pairs
400    }
401
402    /// A name's values as a search of that name alone reads them: a
403    /// title-like name's article written back in front, which music21's
404    /// `_getStringValuesByNamespaceName` does and the rest of a search
405    /// does not, so that a title stored as `Grassy Turf, The` is searched
406    /// as `The Grassy Turf` under `title` and as itself under every field.
407    fn field_texts(&self, unique_name: &str) -> Vec<Cow<'_, str>> {
408        self.get(unique_name)
409            .iter()
410            .map(|value| {
411                if ARTICLED_TITLES.contains(&unique_name) {
412                    Cow::Owned(
413                        crate::text::prepend_article(value.text(), None)
414                            .unwrap_or_else(|_| value.text().to_string()),
415                    )
416                } else {
417                    Cow::Borrowed(value.text())
418                }
419            })
420            .collect()
421    }
422
423    /// The vocabulary's names that are not contributors, sorted, with their
424    /// values: music21's `all(skipContributors=True)` without the names a
425    /// caller made up, which it does not search.
426    fn standard_non_contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
427        self.all_sorted()
428            .into_iter()
429            .filter(|(name, _)| !is_contributor_unique_name(name) && namespace_name(name).is_some())
430    }
431
432    /// The first standard name holding `field` anywhere in it, with the
433    /// first of that name's values: music21's partial field match, which
434    /// stops at the name it finds and searches that one value alone.
435    fn partial_field_match(&self, field: &str) -> Option<(Cow<'_, str>, Option<&str>)> {
436        self.standard_non_contributors().find_map(|(name, value)| {
437            if name.to_lowercase().contains(field) {
438                return Some((Cow::Borrowed(value.text()), Some(name)));
439            }
440            let work_id = old_work_id(name)?;
441            work_id
442                .to_lowercase()
443                .contains(field)
444                .then_some((Cow::Borrowed(value.text()), Some(work_id)))
445        })
446    }
447}
448
449/// Whether a contributor answers to a search field: every contributor where
450/// none was asked for, one whose role holds the field, and a contributor
451/// with no role only under `contributor` itself.
452fn contributor_answers_to(role: Option<&str>, field: Option<&str>) -> bool {
453    let Some(field) = field else {
454        return true;
455    };
456    match role {
457        None => field == "contributor",
458        Some(role) => role.to_lowercase().contains(field),
459    }
460}
461
462/// The unique name an attribute name stands for: the name itself, one of
463/// music21's four grandfathered work ids, or one of its twenty Humdrum
464/// abbreviations. Nothing for a name outside the vocabulary.
465///
466/// Spelling counts, as it does for music21's own attribute access: `otl`
467/// and `title` both name the title, `OTL` names nothing.
468///
469/// ```
470/// use music21::metadata::unique_name_of;
471///
472/// assert_eq!(unique_name_of("title"), Some("title"));
473/// assert_eq!(unique_name_of("otl"), Some("title"));
474/// assert_eq!(unique_name_of("dedication"), Some("dedicatedTo"));
475/// assert_eq!(unique_name_of("OTL"), None);
476/// assert_eq!(unique_name_of("madeUp"), None);
477/// ```
478pub fn unique_name_of(name: &str) -> Option<&'static str> {
479    if let Some((found, _, _)) = STANDARD_PROPERTIES
480        .iter()
481        .find(|(unique, _, _)| *unique == name)
482    {
483        return Some(found);
484    }
485    OLD_MUSIC21_NAMES
486        .iter()
487        .find(|(_, abbreviation, work_id)| *abbreviation == Some(name) || *work_id == Some(name))
488        .map(|(unique, _, _)| *unique)
489}
490
491/// The unique name a search field stands for where it names one outright,
492/// with the spelling it was asked for: the name itself, one of music21's
493/// grandfathered work ids, or one of its Humdrum abbreviations. The field
494/// reaching here is already lowercase, so a name written with a capital in
495/// it is only found by [`Metadata::partial_field_match`], as in music21.
496fn unique_name_of_field(field: &str) -> Option<(&'static str, &'static str)> {
497    if let Some((name, _, _)) = STANDARD_PROPERTIES
498        .iter()
499        .find(|(name, _, _)| *name == field)
500    {
501        return Some((name, name));
502    }
503    OLD_MUSIC21_NAMES
504        .iter()
505        .find_map(
506            |(name, abbreviation, work_id)| match (abbreviation, work_id) {
507                (Some(asked), _) if *asked == field => Some((*name, *asked)),
508                (_, Some(asked)) if *asked == field => Some((*name, *asked)),
509                _ => None,
510            },
511        )
512}
513
514/// The work id a unique name was written as before music21 v8, where it had
515/// one: music21's `UNIQUE_NAME_TO_MUSIC21_WORK_ID`.
516fn old_work_id(unique_name: &str) -> Option<&'static str> {
517    OLD_MUSIC21_NAMES
518        .iter()
519        .find(|(name, _, _)| *name == unique_name)
520        .and_then(|(_, _, work_id)| *work_id)
521}
522
523/// What a [`Metadata::search`] asks of one value's text.
524///
525/// music21 reads a regular expression out of a plain string where it holds
526/// any of `*.|+?{}`, and matches the rest by containment. Nothing here
527/// compiles a pattern -- the crate carries no regular expression engine --
528/// so [`Self::Text`] is music21's plain-string reading alone, and a caller
529/// who wants its regular expression hands over a [`Self::Matcher`] built
530/// from whichever engine they already have.
531pub enum MetadataQuery<'a> {
532    /// A value matches where its text holds this text, either case
533    /// ignored: music21's string query.
534    Text(&'a str),
535    /// A value matches where this answers `true` to its text: music21's
536    /// callable query.
537    Matcher(&'a dyn Fn(&str) -> bool),
538}
539
540impl MetadataQuery<'_> {
541    /// Whether one value's text matches.
542    fn matches(&self, text: &str) -> bool {
543        match self {
544            Self::Text(query) => text.to_lowercase().contains(&query.to_lowercase()),
545            Self::Matcher(matcher) => matcher(text),
546        }
547    }
548}
549
550impl std::fmt::Debug for MetadataQuery<'_> {
551    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552        match self {
553            Self::Text(query) => formatter.debug_tuple("Text").field(query).finish(),
554            Self::Matcher(_) => formatter.write_str("Matcher(..)"),
555        }
556    }
557}
558
559/// What a [`Metadata::search`] found: the text that matched and the field it
560/// answers under. A contributor with a made-up role answers under that
561/// role, and one with no role at all under no field, which is music21's
562/// `(True, None)`.
563#[derive(Clone, Debug, PartialEq, Eq)]
564pub struct MetadataMatch<'a> {
565    field: Option<&'a str>,
566    text: Cow<'a, str>,
567}
568
569impl<'a> MetadataMatch<'a> {
570    /// The field the value was found under.
571    pub fn field(&self) -> Option<&'a str> {
572        self.field
573    }
574
575    /// The text that matched.
576    pub fn text(&self) -> &str {
577        &self.text
578    }
579}
580
581/// The unique names music21 kept an older spelling of: its
582/// `oldMusic21Abbrev` and `oldMusic21WorkId`, both of which
583/// [`Metadata::search`] takes as a field name. Each row is the unique
584/// name, its Humdrum abbreviation and the work id it was written as
585/// before music21 v8.
586pub const OLD_MUSIC21_NAMES: [(&str, Option<&str>, Option<&str>); 21] = [
587    ("alternativeTitle", Some("ota"), None),
588    ("dateCreated", None, Some("date")),
589    ("title", Some("otl"), None),
590    ("textOriginalLanguage", Some("txo"), None),
591    ("textLanguage", Some("txl"), None),
592    ("popularTitle", Some("otp"), None),
593    ("parentTitle", Some("opr"), None),
594    ("actNumber", Some("oac"), None),
595    ("sceneNumber", Some("osc"), None),
596    ("movementNumber", Some("omv"), None),
597    ("movementName", Some("omd"), None),
598    ("opusNumber", Some("ops"), None),
599    ("number", Some("onm"), None),
600    ("volumeNumber", Some("ovm"), Some("volume")),
601    ("dedicatedTo", Some("ode"), Some("dedication")),
602    ("commissionedBy", Some("oco"), Some("commission")),
603    ("countryOfComposition", Some("ocy"), None),
604    ("localeOfComposition", Some("opc"), None),
605    ("groupTitle", Some("gtl"), None),
606    ("associatedWork", Some("gaw"), None),
607    ("collectionDesignation", Some("gco"), None),
608];
609
610/// The names whose values are titles, an article at their end moved to the
611/// front when read as text: music21's `needsArticleNormalization`.
612const ARTICLED_TITLES: [&str; 6] = [
613    "title",
614    "alternativeTitle",
615    "popularTitle",
616    "parentTitle",
617    "movementName",
618    "groupTitle",
619];
620
621/// The eleven vocabulary names whose value is a date rather than a text:
622/// music21's `valueType=DatePrimitive`.
623const DATE_NAMES: [&str; 11] = [
624    "dateAvailable",
625    "dateCreated",
626    "otherDate",
627    "dateAccepted",
628    "dateCopyrighted",
629    "dateSubmitted",
630    "dateIssued",
631    "dateModified",
632    "dateValid",
633    "dateFirstPublished",
634    "electronicReleaseDate",
635];
636
637/// What kind of value a metadata name takes: music21's
638/// `UNIQUE_NAME_TO_VALUE_TYPE`, which is what its `convertValue` reads to
639/// decide what to make of whatever it was handed.
640#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
641#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
642pub enum ValueKind {
643    /// A piece of text: music21's `Text`, which 64 of the names take.
644    Text,
645    /// A text with a role: music21's `Copyright`, which `copyright`
646    /// alone takes.
647    Copyright,
648    /// Somebody who had a hand in the work: music21's `Contributor`,
649    /// which the 54 contributor names take.
650    Contributor,
651    /// A date: music21's `DatePrimitive` and the four classes under it,
652    /// which the eleven date names take.
653    Date,
654    /// A whole number: `fileNumber` alone takes one.
655    Number,
656}
657
658/// What kind of value a unique name takes, and nothing for a name outside
659/// the vocabulary -- which music21 keeps as a text, there being nothing to
660/// say otherwise.
661///
662/// ```
663/// use music21::metadata::{ValueKind, value_kind_of};
664///
665/// assert_eq!(value_kind_of("title"), Some(ValueKind::Text));
666/// assert_eq!(value_kind_of("composer"), Some(ValueKind::Contributor));
667/// assert_eq!(value_kind_of("dateCreated"), Some(ValueKind::Date));
668/// assert_eq!(value_kind_of("copyright"), Some(ValueKind::Copyright));
669/// assert_eq!(value_kind_of("average duration"), None);
670/// ```
671#[must_use]
672pub fn value_kind_of(name: &str) -> Option<ValueKind> {
673    let unique_name = unique_name_of(name).or_else(|| unique_name_from_namespace(name))?;
674    if unique_name == "copyright" {
675        return Some(ValueKind::Copyright);
676    }
677    if unique_name == "fileNumber" {
678        return Some(ValueKind::Number);
679    }
680    if DATE_NAMES.contains(&unique_name) {
681        return Some(ValueKind::Date);
682    }
683    if is_contributor_unique_name(unique_name) {
684        return Some(ValueKind::Contributor);
685    }
686    Some(ValueKind::Text)
687}
688
689/// Whether a unique name is one of the vocabulary's contributor names:
690/// music21's `_isContributorUniqueName`.
691pub fn is_contributor_unique_name(unique_name: &str) -> bool {
692    STANDARD_PROPERTIES
693        .iter()
694        .any(|(name, _, contributor)| *contributor && *name == unique_name)
695}
696
697/// The namespaced name a unique name stands for, `humdrum:OPC` for
698/// `localeOfComposition`: music21's `uniqueNameToNamespaceName`. A name
699/// outside the vocabulary has none.
700pub fn namespace_name(unique_name: &str) -> Option<&'static str> {
701    STANDARD_PROPERTIES
702        .iter()
703        .find(|(name, _, _)| *name == unique_name)
704        .map(|(_, namespaced, _)| *namespaced)
705}
706
707/// The unique name a namespaced name stands for, `localeOfComposition` for
708/// `humdrum:OPC`: music21's `namespaceNameToUniqueName`. A name outside
709/// the vocabulary stands for none.
710///
711/// ```
712/// use music21::metadata::unique_name_from_namespace;
713///
714/// assert_eq!(unique_name_from_namespace("humdrum:OPC"), Some("localeOfComposition"));
715/// assert_eq!(unique_name_from_namespace("soundtracker:SampleName"), None);
716/// ```
717#[must_use]
718pub fn unique_name_from_namespace(namespaced: &str) -> Option<&'static str> {
719    STANDARD_PROPERTIES
720        .iter()
721        .find(|(_, named, _)| *named == namespaced)
722        .map(|(unique, _, _)| *unique)
723}
724
725/// Whether a name is one the vocabulary knows, by its unique name or by
726/// its namespaced one: music21's `isStandardName`. A name of music21's own
727/// older vocabulary -- a work id or its abbreviation -- is not one of
728/// these, as music21's answer is not either.
729///
730/// ```
731/// use music21::metadata::is_standard_name;
732///
733/// assert!(is_standard_name("librettist"));
734/// assert!(is_standard_name("marcrel:LBT"));
735/// assert!(!is_standard_name("average duration"));
736/// assert!(!is_standard_name("soundtracker:SampleName"));
737/// ```
738#[must_use]
739pub fn is_standard_name(name: &str) -> bool {
740    STANDARD_PROPERTIES
741        .iter()
742        .any(|(unique, named, _)| *unique == name || *named == name)
743}
744
745/// The unique name a plural attribute of music21's answers for,
746/// `composer` for `composers`: music21's
747/// `ALL_PLURAL_ATTRIBUTE_NAMES`, each of which answers every value under
748/// the name rather than the first.
749///
750/// ```
751/// use music21::metadata::unique_name_of_plural;
752///
753/// assert_eq!(unique_name_of_plural("composers"), Some("composer"));
754/// assert_eq!(unique_name_of_plural("software"), Some("software"));
755/// assert_eq!(unique_name_of_plural("titles"), None);
756/// ```
757#[must_use]
758pub fn unique_name_of_plural(plural: &str) -> Option<&'static str> {
759    match plural {
760        "composers" => Some("composer"),
761        "librettists" => Some("librettist"),
762        "lyricists" => Some("lyricist"),
763        "software" => Some("software"),
764        _ => None,
765    }
766}
767
768/// music21's `STANDARD_PROPERTY_DESCRIPTIONS`: each property's unique name,
769/// its namespaced name, and whether it names a contributor.
770pub const STANDARD_PROPERTIES: [(&str, &str, bool); 131] = [
771    ("abstract", "dcterms:abstract", false),
772    ("accessRights", "dcterms:accessRights", false),
773    ("alternativeTitle", "dcterms:alternative", false),
774    ("audience", "dcterms:audience", false),
775    ("dateAvailable", "dcterms:available", false),
776    (
777        "bibliographicCitation",
778        "dcterms:bibliographicCitation",
779        false,
780    ),
781    ("conformsTo", "dcterms:conformsTo", false),
782    ("dateCreated", "dcterms:created", false),
783    ("otherDate", "dcterms:date", false),
784    ("dateAccepted", "dcterms:dateAccepted", false),
785    ("dateCopyrighted", "dcterms:dateCopyrighted", false),
786    ("dateSubmitted", "dcterms:dateSubmitted", false),
787    ("description", "dcterms:description", false),
788    ("educationLevel", "dcterms:educationLevel", false),
789    ("extent", "dcterms:extent", false),
790    ("format", "dcterms:format", false),
791    ("hasFormat", "dcterms:hasFormat", false),
792    ("hasPart", "dcterms:hasPart", false),
793    ("hasVersion", "dcterms:hasVersion", false),
794    ("identifier", "dcterms:identifier", false),
795    ("instructionalMethod", "dcterms:instructionalMethod", false),
796    ("isFormatOf", "dcterms:isFormatOf", false),
797    ("isPartOf", "dcterms:isPartOf", false),
798    ("isReferencedBy", "dcterms:isReferencedBy", false),
799    ("isReplacedBy", "dcterms:isReplacedBy", false),
800    ("isRequiredBy", "dcterms:isRequiredBy", false),
801    ("dateIssued", "dcterms:issued", false),
802    ("isVersionOf", "dcterms:isVersionOf", false),
803    ("language", "dcterms:language", false),
804    ("license", "dcterms:license", false),
805    ("medium", "dcterms:medium", false),
806    ("dateModified", "dcterms:modified", false),
807    ("provenance", "dcterms:provenance", false),
808    ("publisher", "dcterms:publisher", true),
809    ("references", "dcterms:references", false),
810    ("relation", "dcterms:relation", false),
811    ("replaces", "dcterms:replaces", false),
812    ("requires", "dcterms:requires", false),
813    ("copyright", "dcterms:rights", false),
814    ("rightsHolder", "dcterms:rightsHolder", true),
815    ("source", "dcterms:source", false),
816    ("subject", "dcterms:subject", false),
817    ("tableOfContents", "dcterms:tableOfContents", false),
818    ("title", "dcterms:title", false),
819    ("type", "dcterms:type", false),
820    ("dateValid", "dcterms:valid", false),
821    ("adapter", "marcrel:ADP", true),
822    ("analyst", "marcrel:ANL", true),
823    ("annotator", "marcrel:ANN", true),
824    ("arranger", "marcrel:ARR", true),
825    ("quotationsAuthor", "marcrel:AQT", true),
826    ("afterwordAuthor", "marcrel:AFT", true),
827    ("dialogAuthor", "marcrel:AUD", true),
828    ("introductionAuthor", "marcrel:AUI", true),
829    ("calligrapher", "marcrel:CLL", true),
830    ("collaborator", "marcrel:CLB", true),
831    ("collotyper", "marcrel:CLT", true),
832    ("commentaryAuthor", "marcrel:CWT", true),
833    ("compiler", "marcrel:COM", true),
834    ("composer", "marcrel:CMP", true),
835    ("conceptor", "marcrel:CCP", true),
836    ("conductor", "marcrel:CND", true),
837    ("otherContributor", "marcrel:CTB", true),
838    ("editor", "marcrel:EDT", true),
839    ("engraver", "marcrel:EGR", true),
840    ("etcher", "marcrel:ETR", true),
841    ("illuminator", "marcrel:ILU", true),
842    ("illustrator", "marcrel:ILL", true),
843    ("instrumentalist", "marcrel:ITR", true),
844    ("librettist", "marcrel:LBT", true),
845    ("lithographer", "marcrel:LTG", true),
846    ("lyricist", "marcrel:LYR", true),
847    ("metalEngraver", "marcrel:MTE", true),
848    ("musician", "marcrel:MUS", true),
849    ("proofreader", "marcrel:PFR", true),
850    ("platemaker", "marcrel:PLT", true),
851    ("printmaker", "marcrel:PRM", true),
852    ("producer", "marcrel:PRO", true),
853    ("responsibleParty", "marcrel:RPY", true),
854    ("scribe", "marcrel:SCR", true),
855    ("singer", "marcrel:SNG", true),
856    ("transcriber", "marcrel:TRC", true),
857    ("translator", "marcrel:TRL", true),
858    ("woodEngraver", "marcrel:WDE", true),
859    ("woodCutter", "marcrel:WDC", true),
860    ("accompanyingMaterialWriter", "marcrel:WAM", true),
861    ("distributor", "marcrel:DST", true),
862    ("software", "musicxml:software", false),
863    ("textOriginalLanguage", "humdrum:TXO", false),
864    ("textLanguage", "humdrum:TXL", false),
865    ("popularTitle", "humdrum:OTP", false),
866    ("parentTitle", "humdrum:OPR", false),
867    ("actNumber", "humdrum:OAC", false),
868    ("sceneNumber", "humdrum:OSC", false),
869    ("movementNumber", "humdrum:OMV", false),
870    ("movementName", "humdrum:OMD", false),
871    ("opusNumber", "humdrum:OPS", false),
872    ("number", "humdrum:ONM", false),
873    ("volumeNumber", "humdrum:OVM", false),
874    ("dedicatedTo", "humdrum:ODE", false),
875    ("commissionedBy", "humdrum:OCO", false),
876    ("countryOfComposition", "humdrum:OCY", false),
877    ("localeOfComposition", "humdrum:OPC", false),
878    ("groupTitle", "humdrum:GTL", false),
879    ("associatedWork", "humdrum:GAW", false),
880    ("collectionDesignation", "humdrum:GCO", false),
881    ("attributedComposer", "humdrum:COA", true),
882    ("suspectedComposer", "humdrum:COS", true),
883    ("composerAlias", "humdrum:COL", true),
884    ("composerCorporate", "humdrum:COC", true),
885    ("orchestrator", "humdrum:LOR", true),
886    ("firstPublisher", "humdrum:PPR", true),
887    ("dateFirstPublished", "humdrum:PDT", false),
888    ("publicationTitle", "humdrum:PTL", false),
889    ("placeFirstPublished", "humdrum:PPP", false),
890    ("publishersCatalogNumber", "humdrum:PC#", false),
891    ("scholarlyCatalogName", "humdrum:SCA", false),
892    ("scholarlyCatalogAbbreviation", "humdrum:SCT", false),
893    ("manuscriptSourceName", "humdrum:SMS", false),
894    ("manuscriptLocation", "humdrum:SML", false),
895    ("manuscriptAccessAcknowledgement", "humdrum:SMA", false),
896    ("originalDocumentOwner", "humdrum:YOO", true),
897    ("originalEditor", "humdrum:YOE", true),
898    ("electronicEditor", "humdrum:EED", true),
899    ("electronicEncoder", "humdrum:ENC", true),
900    ("electronicPublisher", "humdrum:YEP", true),
901    ("electronicReleaseDate", "humdrum:YER", false),
902    ("fileFormat", "m21FileInfo:fileFormat", false),
903    ("filePath", "m21FileInfo:filePath", false),
904    ("corpusFilePath", "m21FileInfo:corpusFilePath", false),
905    ("fileNumber", "m21FileInfo:fileNumber", false),
906];
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911
912    /// Every answer here is music21's own for the same metadata.
913    #[test]
914    fn the_best_title_is_the_first_one_said() {
915        let title = |name: &str, text: &str| {
916            let mut said = Metadata::new();
917            said.add_text(name, text);
918            said.best_title()
919        };
920        assert_eq!(
921            title("title", "Third Symphony").as_deref(),
922            Some("Third Symphony")
923        );
924        assert_eq!(title("popularTitle", "Eroica").as_deref(), Some("Eroica"));
925        assert_eq!(
926            title("alternativeTitle", "Bonaparte").as_deref(),
927            Some("Bonaparte")
928        );
929        assert_eq!(title("movementName", "Allegro").as_deref(), Some("Allegro"));
930        assert_eq!(Metadata::new().best_title(), None);
931
932        // The title itself wins over the one it is popularly called.
933        let mut both = Metadata::new();
934        both.add_text("popularTitle", "Eroica");
935        both.add_text("title", "Third Symphony");
936        assert_eq!(both.best_title().as_deref(), Some("Third Symphony"));
937    }
938
939    /// Every answer here is music21's own for the same contributors.
940    #[test]
941    fn contributors_are_found_by_their_role() {
942        let mut said = Metadata::new();
943        said.add_text("title", "Violin Concerto");
944        said.add_contributor("composer", "Price, Florence");
945        // A contributor with no role of its own, as a MusicXML file may
946        // name one: music21 files it under `otherContributor`.
947        said.add("otherContributor", MetadataValue::new("Baron van Swieten"));
948
949        let names = |role: Option<&str>| -> Vec<String> {
950            said.contributors_by_role(role)
951                .iter()
952                .map(|value| value.text().to_string())
953                .collect()
954        };
955        assert_eq!(names(Some("composer")), ["Price, Florence"]);
956        assert_eq!(names(None), ["Baron van Swieten"]);
957        assert!(names(Some("lyricist")).is_empty());
958
959        let mut two = Metadata::new();
960        two.add_contributor("composer", "A");
961        two.add_contributor("composer", "B");
962        assert_eq!(
963            two.contributors_by_role(Some("composer"))
964                .iter()
965                .map(|value| value.text())
966                .collect::<Vec<&str>>(),
967            ["A", "B"]
968        );
969    }
970
971    #[test]
972    fn names_keep_the_order_they_were_first_given() {
973        let mut md = Metadata::new();
974        md.add_text("software", "Finale");
975        md.add_text("movementName", "Grave");
976        md.add_text("software", "Dolet");
977        let all: Vec<(&str, &str)> = md.all().map(|(name, value)| (name, value.text())).collect();
978        assert_eq!(
979            all,
980            [
981                ("software", "Finale"),
982                ("software", "Dolet"),
983                ("movementName", "Grave")
984            ]
985        );
986    }
987
988    #[test]
989    fn a_role_outside_the_vocabulary_is_another_contributor() {
990        let mut md = Metadata::new();
991        md.add_contributor("composer", "Caccini");
992        md.add_contributor("tuba player", "Somebody");
993        let names: Vec<&str> = md.contributors().map(|(name, _)| name).collect();
994        assert_eq!(names, ["composer", "otherContributor"]);
995        assert_eq!(md.get("otherContributor")[0].role(), Some("tuba player"));
996    }
997
998    #[test]
999    fn a_search_of_one_name_reads_a_title_with_its_article_in_front() {
1000        let mut md = Metadata::new();
1001        md.add_text("title", "Grassy Turf, The");
1002        // music21 normalizes the article only where the field is named
1003        // outright, and leaves the stored text alone everywhere else.
1004        let query = MetadataQuery::Text("The Grassy");
1005        assert_eq!(
1006            md.search(&query, Some("title"))
1007                .map(|found| found.text().to_string()),
1008            Some("The Grassy Turf".to_string())
1009        );
1010        assert!(md.search(&query, None).is_none());
1011        assert!(md.search(&query, Some("titl")).is_none());
1012    }
1013
1014    #[test]
1015    fn a_contributor_answers_under_the_role_it_was_given() {
1016        let mut md = Metadata::new();
1017        md.add_contributor("tuba player", "Somebody");
1018        let query = MetadataQuery::Text("somebody");
1019        assert_eq!(
1020            md.search(&query, None).and_then(|found| found.field()),
1021            Some("tuba player")
1022        );
1023        assert_eq!(
1024            md.search(&query, Some("tuba"))
1025                .and_then(|found| found.field()),
1026            Some("tuba player")
1027        );
1028        // A role of its own is not reached under `contributor`, as in
1029        // music21, which only asks that of a contributor with no role.
1030        assert!(md.search(&query, Some("contributor")).is_none());
1031    }
1032
1033    #[test]
1034    fn a_matcher_searches_where_music21_would_read_a_pattern() {
1035        let mut md = Metadata::new();
1036        md.add_text("title", "Maple Leaf Rag");
1037        md.add_contributor("composer", "Joplin, Scott");
1038        let ends_in_rag = |text: &str| text.ends_with("Rag");
1039        let found = md
1040            .search(&MetadataQuery::Matcher(&ends_in_rag), None)
1041            .expect("the title");
1042        assert_eq!(
1043            (found.field(), found.text()),
1044            (Some("title"), "Maple Leaf Rag")
1045        );
1046        assert!(
1047            md.search(&MetadataQuery::Matcher(&ends_in_rag), Some("composer"))
1048                .is_none()
1049        );
1050    }
1051
1052    #[test]
1053    fn the_vocabulary_names_its_namespaces() {
1054        assert_eq!(namespace_name("localeOfComposition"), Some("humdrum:OPC"));
1055        assert_eq!(namespace_name("dateCreated"), Some("dcterms:created"));
1056        assert_eq!(namespace_name("composer"), Some("marcrel:CMP"));
1057        assert_eq!(namespace_name("madeUp"), None);
1058        assert!(is_contributor_unique_name("lyricist"));
1059        assert!(!is_contributor_unique_name("title"));
1060    }
1061}