Skip to main content

music21_rs/chord/tables/
mod.rs

1//! Look-up over music21's Forte tables of set classes.
2//!
3//! These are the tables every set-class question a [`Chord`] answers is read
4//! out of — its prime form, its Forte name, its interval vector — and
5//! music21 exposes them as `chord.tables` so that a caller holding an
6//! address can read the same values with no chord to ask. A set class is
7//! named by its cardinality and its Forte class number, with an inversion of
8//! `1` or `-1` for the two halves of an inversionally related pair and `0`
9//! for a class that is its own inversion; the inversion may be left out, and
10//! the table then says which it is.
11//!
12//! The tables are generated from music21's own `chord/tables.py`; see
13//! `xtask verify-tables`.
14//!
15//! [`Chord`]: crate::Chord
16
17mod generated;
18
19use crate::chord::ChordTableAddress;
20use crate::defaults::IntegerType;
21use crate::error::Error;
22
23use generated::*;
24
25/// An address as the table search hands one back, before it is read into a
26/// [`ChordTableAddress`]: cardinality, Forte class, inversion, and the pitch
27/// class the prime form was transposed away from.
28pub(crate) type RawAddress = (u8, u8, i8, Option<u8>);
29
30// TNI structures are defined as
31// [0] = tuple of pitch classes (0-11)
32// [1] = 6-tuple of interval class vector (ICV)
33// [2] = 8-tuple of invariance vector (Robert Morris) -- see below
34// [3] = index of Z-relation (0=none)
35type PitchClasses = [bool; 12];
36type IntervalClassVector = [u8; 6];
37type InvarianceVector = [u8; 8];
38type ZRelation = u8;
39
40type TNIStructure = (
41    PitchClasses,
42    IntervalClassVector,
43    InvarianceVector,
44    ZRelation,
45);
46
47type Pcivicv = (PitchClasses, InvarianceVector, IntervalClassVector);
48
49trait TNITupleExt {
50    fn pitches(&self) -> PitchClasses;
51    fn pitch_classes(&self) -> PitchClasses;
52    fn invariance_vector(&self) -> InvarianceVector;
53    fn z_relation(&self) -> ZRelation;
54}
55
56impl TNITupleExt for TNIStructure {
57    fn pitches(&self) -> PitchClasses {
58        self.0
59    }
60
61    fn pitch_classes(&self) -> PitchClasses {
62        self.pitches()
63    }
64
65    fn invariance_vector(&self) -> InvarianceVector {
66        self.2
67    }
68
69    fn z_relation(&self) -> ZRelation {
70        self.3
71    }
72}
73
74#[repr(i8)]
75#[derive(Copy, Clone, Eq, Hash, PartialEq, Debug)]
76enum Sign {
77    NegativeOne = -1,
78    Zero = 0,
79    One = 1,
80}
81
82impl Sign {
83    pub(crate) fn from_i8(i: i8) -> Option<Self> {
84        match i {
85            0 => Some(Sign::Zero),
86            1 => Some(Sign::One),
87            -1 => Some(Sign::NegativeOne),
88            _ => None,
89        }
90    }
91
92    fn as_i8(&self) -> i8 {
93        *self as i8
94    }
95}
96
97type U8SB = (u8, Sign);
98type U8U8SB = (u8, u8, Sign);
99
100const CARDINALITIES: usize = 13;
101
102type Forte = [&'static [Option<TNIStructure>]; CARDINALITIES];
103type CardinalityToChordMembers = [&'static [(U8SB, Pcivicv)]; CARDINALITIES];
104type ForteNumberWithInversionToIndex = &'static [(U8U8SB, u8)];
105type TnIndexToChordInfo = &'static [(U8U8SB, Option<&'static [&'static str]>)];
106type MaximumIndexNumberWithoutInversionEquivalence = [u8; CARDINALITIES];
107type MaximumIndexNumberWithInversionEquivalence = [u8; CARDINALITIES];
108
109#[derive(Debug, Clone)]
110pub(crate) struct KnownChordTableEntry {
111    pub(crate) cardinality: u8,
112    pub(crate) common_names: Vec<&'static str>,
113    pub(crate) forte_class: String,
114    pub(crate) normal_form: Vec<u8>,
115    pub(crate) interval_class_vector: Vec<u8>,
116}
117
118/// Whether a Forte index stands for a set class that is its own inversion.
119///
120/// [`forte_index_to_inversions_available`] answers the same question with a
121/// `Vec`, which the address search cannot afford: it asks once per candidate
122/// index, and there are up to fifty of those per cardinality.
123fn forte_index_is_inversion_equivalent(card: usize, index: u8) -> Result<bool, Error> {
124    if !(1..=12).contains(&card) {
125        return Err(Error::ChordTables(format!("cardinality {card} not valid")));
126    }
127    if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card] {
128        return Err(Error::ChordTables(format!("index {index} not valid")));
129    }
130    Ok(FORTE[card]
131        .get(index as usize)
132        .and_then(Option::as_ref)
133        .is_some_and(|entry| entry.invariance_vector()[1] > 0))
134}
135
136fn forte_index_to_inversions_available(card: usize, index: u8) -> Result<Vec<Sign>, Error> {
137    if !(1..=12).contains(&card) {
138        return Err(Error::ChordTables(format!("cardinality {card} not valid")));
139    }
140    if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card] {
141        return Err(Error::ChordTables(format!("index {index} not valid")));
142    }
143
144    let mut inversions = vec![];
145    if let Some(entry) = FORTE[card].get(index as usize).and_then(Option::as_ref) {
146        // second value stored inversion status
147        if entry.invariance_vector()[1] > 0 {
148            inversions.push(Sign::Zero);
149        } else {
150            inversions.push(Sign::NegativeOne);
151            inversions.push(Sign::One);
152        }
153    }
154    Ok(inversions)
155}
156
157fn validate_address(address: (u8, u8, Option<i8>)) -> Result<(u8, u8, Sign), Error> {
158    let card = address.0;
159    let index = address.1;
160    // An inversion is one of three things, and anything else is the caller's
161    // mistake rather than an inversion left out — reading `-30` as "say
162    // nothing" would answer for a set class nobody asked about.
163    let inversion = match address.2 {
164        None => None,
165        Some(given) => Some(
166            Sign::from_i8(given)
167                .ok_or_else(|| Error::ChordTables(format!("inversion {given} not valid")))?,
168        ),
169    };
170
171    if !(1..=12).contains(&card) {
172        return Err(Error::ChordTables(format!("cardinality {card} not valid")));
173    }
174
175    if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card as usize] {
176        return Err(Error::ChordTables(format!("index {index} not valid")));
177    }
178
179    let inversions_available = forte_index_to_inversions_available(card as usize, index)?;
180
181    let resolved_inversion = if let Some(inv) = inversion {
182        if inversions_available.contains(&inv) {
183            inv
184        } else {
185            return Err(Error::ChordTables(format!(
186                "inversion {} not valid",
187                inv.as_i8()
188            )));
189        }
190    } else if inversions_available.contains(&Sign::Zero) {
191        Sign::Zero
192    } else {
193        Sign::One
194    };
195
196    Ok((card, index, resolved_inversion))
197}
198
199fn bool_vec_to_pitch_classes(v: &[bool]) -> Vec<u8> {
200    v.iter()
201        .enumerate()
202        .filter_map(
203            |(idx, present)| {
204                if *present { Some(idx as u8) } else { None }
205            },
206        )
207        .collect()
208}
209
210fn pitch_classes_to_bools(pcs: &[u8]) -> [bool; 12] {
211    let mut out = [false; 12];
212    for pc in pcs {
213        out[*pc as usize % 12] = true;
214    }
215    out
216}
217
218pub(crate) fn seek_chord_tables_address(ordered_pitch_classes: &[u8]) -> Result<RawAddress, Error> {
219    if ordered_pitch_classes.is_empty() {
220        return Err(Error::ChordTables(
221            "cannot access chord tables address for Chord with 0 pitches".to_string(),
222        ));
223    }
224
225    let card = ordered_pitch_classes.len() as u8;
226    if card == 1 {
227        return Ok((1, 1, 0, Some(ordered_pitch_classes[0] % 12)));
228    }
229    if card == 12 {
230        return Ok((12, 1, 0, Some(0)));
231    }
232
233    // Every rotation of the set, transposed to start on zero, and its
234    // inversion. A chord has at most twelve pitch classes, so all of this
235    // fits on the stack: the search runs on every question a caller asks a
236    // chord about its set class, and heap traffic here was most of its cost.
237    let count = ordered_pitch_classes.len();
238    let mut candidates = [([false; 12], [false; 12], 0_u8); 12];
239    let mut transposed = [0_u8; 12];
240    let mut inverted = [0_u8; 12];
241    for rot in 0..count {
242        let original_pc = ordered_pitch_classes[rot] % 12;
243        for (offset, slot) in transposed[..count].iter_mut().enumerate() {
244            let pitch_class = ordered_pitch_classes[(rot + offset) % count];
245            *slot = ((IntegerType::from(pitch_class) - IntegerType::from(original_pc))
246                .rem_euclid(12)) as u8;
247        }
248        // the inversion is each degree taken downwards, read back to front
249        for offset in 0..count {
250            inverted[offset] = (12 - transposed[count - 1 - offset]) % 12;
251        }
252        let shift = (12 - inverted[0]) % 12;
253        for slot in inverted[..count].iter_mut() {
254            *slot = (*slot + shift) % 12;
255        }
256        candidates[rot] = (
257            pitch_classes_to_bools(&transposed[..count]),
258            pitch_classes_to_bools(&inverted[..count]),
259            original_pc,
260        );
261    }
262    let candidates = &candidates[..count];
263
264    for (index_candidate, data_line) in FORTE[card as usize].iter().enumerate().skip(1) {
265        let Some(data_line) = data_line else {
266            continue;
267        };
268        let data_line_pcs = data_line.pitch_classes();
269        let is_own_inversion =
270            || forte_index_is_inversion_equivalent(card as usize, index_candidate as u8);
271
272        for (candidate, candidate_inversion, candidate_original_pc) in candidates {
273            if data_line_pcs == *candidate {
274                let inversion = if is_own_inversion()? { 0 } else { 1 };
275                return Ok((
276                    card,
277                    index_candidate as u8,
278                    inversion,
279                    Some(*candidate_original_pc),
280                ));
281            }
282            if data_line_pcs == *candidate_inversion {
283                let inversion = if is_own_inversion()? { 0 } else { -1 };
284                return Ok((
285                    card,
286                    index_candidate as u8,
287                    inversion,
288                    Some(*candidate_original_pc),
289                ));
290            }
291        }
292    }
293
294    Err(Error::ChordTables(format!(
295        "cannot find a chord table address for {ordered_pitch_classes:?}"
296    )))
297}
298
299pub(crate) fn address_to_common_names(
300    address: RawAddress,
301) -> Result<Option<Vec<&'static str>>, Error> {
302    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
303    Ok(find_tn_index_to_chord_info(card, index, inversion).map(<[_]>::to_vec))
304}
305
306pub(crate) fn address_to_forte_name(
307    address: RawAddress,
308    classification: &str,
309) -> Result<String, Error> {
310    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
311    let inversion_suffix = match classification.to_ascii_lowercase().as_str() {
312        "tn" => match inversion {
313            Sign::NegativeOne => "B",
314            Sign::One => "A",
315            Sign::Zero => "",
316        },
317        _ => "",
318    };
319    Ok(format!("{card}-{index}{inversion_suffix}"))
320}
321
322/// The prime form of a set class, which is the same whichever inversion the
323/// address names.
324pub(crate) fn prime_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
325    let (card, index, inversion) = validate_address((address.0, address.1, None))?;
326    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
327        Error::ChordTables(format!(
328            "cannot resolve prime form for address ({card}, {index})"
329        ))
330    })?;
331    Ok(bool_vec_to_pitch_classes(&entry.0))
332}
333
334/// The transposed normal form for a cardinality and Forte number, with the
335/// inversion left to the table's default when `None`.
336pub(crate) fn transposed_normal_form(
337    cardinality: u8,
338    index: u8,
339    inversion: Option<i8>,
340) -> Result<Vec<u8>, Error> {
341    let (card, index, inversion) = validate_address((cardinality, index, inversion))?;
342    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
343        Error::ChordTables(format!(
344            "cannot resolve normal form for address ({card}, {index}, {})",
345            inversion.as_i8()
346        ))
347    })?;
348    Ok(bool_vec_to_pitch_classes(&entry.0))
349}
350
351pub(crate) fn transposed_normal_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
352    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
353    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
354        Error::ChordTables(format!(
355            "cannot resolve normal form for address ({card}, {index}, {})",
356            inversion.as_i8()
357        ))
358    })?;
359    Ok(bool_vec_to_pitch_classes(&entry.0))
360}
361
362pub(crate) fn interval_class_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
363    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
364    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
365        Error::ChordTables(format!(
366            "cannot resolve interval class vector for address ({card}, {index}, {})",
367            inversion.as_i8()
368        ))
369    })?;
370    Ok(entry.2.to_vec())
371}
372
373pub(crate) fn invariance_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
374    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
375    let entry = FORTE[card as usize]
376        .get(index as usize)
377        .and_then(Option::as_ref)
378        .ok_or_else(|| {
379            Error::ChordTables(format!(
380                "cannot resolve invariance vector for address ({card}, {index}, {})",
381                inversion.as_i8()
382            ))
383        })?;
384    Ok(entry.invariance_vector().to_vec())
385}
386
387pub(crate) fn z_relation_from_address(address: RawAddress) -> Result<Option<String>, Error> {
388    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
389    let entry = FORTE[card as usize]
390        .get(index as usize)
391        .and_then(Option::as_ref)
392        .ok_or_else(|| {
393            Error::ChordTables(format!(
394                "cannot resolve z-relation for address ({card}, {index}, {})",
395                inversion.as_i8()
396            ))
397        })?;
398    let z_relation = entry.z_relation();
399    if z_relation == 0 {
400        Ok(None)
401    } else {
402        Ok(Some(format!("{card}-{}", z_relation)))
403    }
404}
405
406pub(crate) fn known_chord_table_entries() -> Vec<KnownChordTableEntry> {
407    let mut entries = TN_INDEX_TO_CHORD_INFO
408        .iter()
409        .filter_map(|&((cardinality, index, inversion), common_names)| {
410            let common_names = common_names.unwrap_or_default().to_vec();
411            let address = (cardinality, index, inversion.as_i8(), None);
412            Some((
413                (cardinality, index, inversion.as_i8()),
414                KnownChordTableEntry {
415                    cardinality,
416                    common_names,
417                    forte_class: address_to_forte_name(address, "tn").ok()?,
418                    normal_form: transposed_normal_form_from_address(address).ok()?,
419                    interval_class_vector: interval_class_vector_from_address(address).ok()?,
420                },
421            ))
422        })
423        .collect::<Vec<_>>();
424
425    entries.sort_by_key(|(sort_key, _)| *sort_key);
426    entries.into_iter().map(|(_, entry)| entry).collect()
427}
428
429fn find_cardinality_member(card: u8, index: u8, inversion: Sign) -> Option<&'static Pcivicv> {
430    CARDINALITY_TO_CHORD_MEMBERS
431        .get(card as usize)?
432        .iter()
433        .find_map(|(key, value)| (*key == (index, inversion)).then_some(value))
434}
435
436fn find_tn_index_to_chord_info(
437    card: u8,
438    index: u8,
439    inversion: Sign,
440) -> Option<&'static [&'static str]> {
441    TN_INDEX_TO_CHORD_INFO
442        .iter()
443        .find_map(|(key, names)| (*key == (card, index, inversion)).then_some(*names))
444        .flatten()
445}
446
447// include!("./generated.rs");
448
449// ---------------------------------------------------------------------------
450// The tables as music21 exposes them: a set class named by its cardinality
451// and Forte class number, asked about without a chord in hand.
452// ---------------------------------------------------------------------------
453
454/// The inversions a set class comes in: `[0]` for one that is its own
455/// inversion, and `[-1, 1]` for the two halves of an inversionally related
456/// pair.
457///
458/// music21's `forteIndexToInversionsAvailable`.
459///
460/// # Errors
461///
462/// When the cardinality or the class number names no set class.
463pub fn inversions_available(cardinality: u8, forte_class: u8) -> Result<Vec<i8>, Error> {
464    Ok(
465        forte_index_to_inversions_available(cardinality as usize, forte_class)?
466            .into_iter()
467            .map(|sign| sign.as_i8())
468            .collect(),
469    )
470}
471
472/// Reads an address, filling in the inversion where one was not given:
473/// music21's `_validateAddress`.
474///
475/// A class that is its own inversion answers nought; one that is not answers
476/// the first of its pair.
477///
478/// # Errors
479///
480/// When the cardinality, the class number or the inversion names no set
481/// class.
482pub fn read_address(
483    cardinality: u8,
484    forte_class: u8,
485    inversion: Option<i8>,
486) -> Result<(u8, u8, i8), Error> {
487    let (cardinality, forte_class, inversion) =
488        validate_address((cardinality, forte_class, inversion))?;
489    Ok((cardinality, forte_class, inversion.as_i8()))
490}
491
492/// The set class's normal form, transposed to start on zero: music21's
493/// `addressToTransposedNormalForm`.
494///
495/// # Errors
496///
497/// When the address names no set class.
498pub fn normal_form(
499    cardinality: u8,
500    forte_class: u8,
501    inversion: Option<i8>,
502) -> Result<Vec<u8>, Error> {
503    transposed_normal_form(cardinality, forte_class, inversion)
504}
505
506/// The set class's prime form, which is the same whichever inversion the
507/// address names: music21's `addressToPrimeForm`.
508///
509/// # Errors
510///
511/// When the address names no set class.
512pub fn prime_form(cardinality: u8, forte_class: u8) -> Result<Vec<u8>, Error> {
513    transposed_normal_form(cardinality, forte_class, None)
514}
515
516/// The set class's interval class vector, six counts of how many intervals
517/// of each class it holds: music21's `addressToIntervalVector`.
518///
519/// # Errors
520///
521/// When the address names no set class.
522pub fn interval_class_vector(
523    cardinality: u8,
524    forte_class: u8,
525    inversion: Option<i8>,
526) -> Result<Vec<u8>, Error> {
527    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
528    interval_class_vector_from_address((cardinality, forte_class, inversion, None))
529}
530
531/// Every set class with this interval vector, as cardinality and Forte class
532/// number pairs: music21's `intervalVectorToAddress`.
533///
534/// More than one comes back for a Z-related pair — the all-interval
535/// tetrachord is both 4-15 and 4-29 — and none at all for a vector no set
536/// class has. The inversion is not part of the answer, since a vector says
537/// nothing about which half of a pair is meant.
538///
539/// # Errors
540///
541/// When the vector is not six counts long.
542pub fn set_classes_with_interval_vector(vector: &[u8]) -> Result<Vec<(u8, u8)>, Error> {
543    if vector.len() != 6 {
544        return Err(Error::ChordTables(
545            "Vector must have exactly six entries".to_string(),
546        ));
547    }
548    let mut found = Vec::new();
549    for (cardinality, classes) in FORTE.iter().enumerate().skip(1) {
550        for (forte_class, entry) in classes.iter().enumerate() {
551            let Some(entry) = entry else {
552                continue;
553            };
554            if entry.1 == vector {
555                found.push((cardinality as u8, forte_class as u8));
556            }
557        }
558    }
559    Ok(found)
560}
561
562/// The set class Z-related to this one — the other with the same interval
563/// vector — where there is one: music21's `addressToZAddress`.
564///
565/// The inversion comes back read the way [`read_address`] reads one.
566///
567/// # Errors
568///
569/// When the address names no set class.
570pub fn z_related_class(cardinality: u8, forte_class: u8) -> Result<Option<(u8, u8, i8)>, Error> {
571    let (cardinality, forte_class, _) = read_address(cardinality, forte_class, None)?;
572    let related = FORTE[cardinality as usize]
573        .get(forte_class as usize)
574        .and_then(Option::as_ref)
575        .map_or(0, TNITupleExt::z_relation);
576    if related == 0 {
577        return Ok(None);
578    }
579    Some(read_address(cardinality, related, None)).transpose()
580}
581
582/// What the set class is commonly called, where it is called anything:
583/// music21's `addressToCommonNames`.
584///
585/// More than one name comes back where more than one is in use, and nothing
586/// where the class has no common name. The names take no account of spelling
587/// or inversion, so a minor triad answers "major triad"; [`Chord::common_name`]
588/// is the one that reads the chord as it is written.
589///
590/// # Errors
591///
592/// When the address names no set class.
593///
594/// [`Chord::common_name`]: crate::Chord::common_name
595pub fn common_names(
596    cardinality: u8,
597    forte_class: u8,
598    inversion: Option<i8>,
599) -> Result<Option<Vec<&'static str>>, Error> {
600    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
601    address_to_common_names((cardinality, forte_class, inversion, None))
602}
603
604/// The set class's Forte name, such as `8-15B`: music21's
605/// `addressToForteName`.
606///
607/// `inversional_equivalence` is music21's `'tni'` classification, which
608/// leaves off the `A` and `B` that tell the two halves of a pair apart.
609///
610/// # Errors
611///
612/// When the address names no set class.
613pub fn forte_name(
614    cardinality: u8,
615    forte_class: u8,
616    inversion: Option<i8>,
617    inversional_equivalence: bool,
618) -> Result<String, Error> {
619    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
620    address_to_forte_name(
621        (cardinality, forte_class, inversion, None),
622        if inversional_equivalence { "tni" } else { "tn" },
623    )
624}
625
626/// Where a set of pitch classes sits in the tables: music21's
627/// `seekChordTablesAddress`.
628///
629/// The pitch classes are the chord's own, in order and without repeats —
630/// [`Chord::pitch_classes`] is what a chord hands over.
631///
632/// # Errors
633///
634/// When there are no pitch classes at all, or more than the tables hold.
635///
636/// [`Chord::pitch_classes`]: crate::Chord::pitch_classes
637pub fn address_of_pitch_classes(pitch_classes: &[u8]) -> Result<ChordTableAddress, Error> {
638    let (cardinality, forte_class, inversion, original) = seek_chord_tables_address(pitch_classes)?;
639    Ok(ChordTableAddress {
640        cardinality,
641        forte_class,
642        inversion,
643        pitch_class_original: original.unwrap_or(0),
644    })
645}
646
647#[cfg(test)]
648mod tests {
649
650    #[test]
651    fn the_address_helpers_read_the_tables_for_a_major_triad() {
652        use super::{
653            interval_class_vector_from_address, invariance_vector_from_address,
654            prime_form_from_address, transposed_normal_form_from_address, z_relation_from_address,
655        };
656
657        let major = (3, 11, -1, Some(0));
658        assert_eq!(prime_form_from_address(major).unwrap(), [0, 3, 7]);
659        assert_eq!(
660            transposed_normal_form_from_address(major).unwrap(),
661            [0, 4, 7]
662        );
663        assert_eq!(
664            interval_class_vector_from_address(major).unwrap(),
665            [0, 0, 1, 1, 1, 0]
666        );
667        assert_eq!(invariance_vector_from_address(major).unwrap().len(), 8);
668        assert_eq!(z_relation_from_address(major).unwrap(), None);
669        assert_eq!(
670            z_relation_from_address((4, 15, 1, Some(0)))
671                .unwrap()
672                .as_deref(),
673            Some("4-29")
674        );
675        assert!(prime_form_from_address((3, 99, 0, None)).is_err());
676    }
677
678    use super::{Sign, find_cardinality_member};
679
680    #[test]
681    fn cardinality_to_chord_members_include_major_triad() {
682        let member = find_cardinality_member(3, 11, Sign::NegativeOne).unwrap();
683        assert_eq!(
684            member.0,
685            [
686                true, false, false, false, true, false, false, true, false, false, false, false
687            ]
688        );
689        assert_eq!(member.2, [0, 0, 1, 1, 1, 0]);
690    }
691
692    /// music21's own answers, out of the docstrings of `chord/tables.py`.
693    #[test]
694    fn the_tables_answer_what_music21_answers() {
695        use super::*;
696
697        assert_eq!(inversions_available(3, 1).unwrap(), [0]);
698        assert_eq!(inversions_available(3, 2).unwrap(), [-1, 1]);
699        assert_eq!(inversions_available(3, 12).unwrap(), [0]);
700        assert!(inversions_available(20, 1).is_err());
701        assert!(inversions_available(8, 200).is_err());
702
703        // An inversion left out is read off the table: nought where the
704        // class is its own inversion, and the first of the pair otherwise.
705        assert_eq!(read_address(3, 1, Some(0)).unwrap(), (3, 1, 0));
706        assert_eq!(read_address(2, 3, None).unwrap(), (2, 3, 0));
707        assert_eq!(read_address(3, 12, None).unwrap(), (3, 12, 0));
708        assert!(read_address(8, 3, Some(-30)).is_err());
709
710        assert_eq!(normal_form(3, 1, Some(0)).unwrap(), [0, 1, 2]);
711        assert_eq!(normal_form(3, 11, Some(-1)).unwrap(), [0, 4, 7]);
712        assert_eq!(normal_form(3, 11, Some(1)).unwrap(), [0, 3, 7]);
713        assert_eq!(normal_form(3, 11, None).unwrap(), [0, 3, 7]);
714
715        // The prime form is the same whichever half of the pair is named.
716        assert_eq!(prime_form(3, 11).unwrap(), [0, 3, 7]);
717        assert_eq!(prime_form(3, 1).unwrap(), [0, 1, 2]);
718
719        assert_eq!(
720            interval_class_vector(3, 1, Some(0)).unwrap(),
721            [2, 1, 0, 0, 0, 0]
722        );
723        assert_eq!(
724            interval_class_vector(3, 11, Some(-1)).unwrap(),
725            [0, 0, 1, 1, 1, 0]
726        );
727        assert_eq!(
728            interval_class_vector(4, 29, None).unwrap(),
729            [1, 1, 1, 1, 1, 1]
730        );
731
732        assert_eq!(
733            set_classes_with_interval_vector(&[7, 6, 5, 4, 4, 2]).unwrap(),
734            [(8, 1)]
735        );
736        assert_eq!(
737            set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 1]).unwrap(),
738            [(5, 10)]
739        );
740        assert!(
741            set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 99])
742                .unwrap()
743                .is_empty()
744        );
745        // The all-interval tetrachord is a Z-related pair, so both come back.
746        assert_eq!(
747            set_classes_with_interval_vector(&[1, 1, 1, 1, 1, 1]).unwrap(),
748            [(4, 15), (4, 29)]
749        );
750        assert!(set_classes_with_interval_vector(&[0, 2, 4]).is_err());
751
752        assert_eq!(z_related_class(5, 12).unwrap(), Some((5, 36, 1)));
753        assert_eq!(z_related_class(5, 36).unwrap(), Some((5, 12, 0)));
754        assert_eq!(z_related_class(3, 11).unwrap(), None);
755        assert_eq!(z_related_class(8, 29).unwrap(), Some((8, 15, 1)));
756
757        assert_eq!(
758            common_names(3, 1, Some(0)).unwrap(),
759            Some(vec!["chromatic trimirror"])
760        );
761        assert_eq!(
762            common_names(3, 11, Some(-1)).unwrap(),
763            Some(vec!["major triad"])
764        );
765        assert_eq!(
766            common_names(7, 33, None).unwrap(),
767            Some(vec!["Neapolitan-major mode", "leading-whole-tone mode"])
768        );
769
770        assert_eq!(forte_name(8, 15, Some(-1), false).unwrap(), "8-15B");
771        assert_eq!(forte_name(8, 15, None, false).unwrap(), "8-15A");
772        assert_eq!(forte_name(3, 12, None, false).unwrap(), "3-12");
773        assert_eq!(forte_name(8, 15, None, true).unwrap(), "8-15");
774        assert_eq!(forte_name(5, 37, None, false).unwrap(), "5-37");
775
776        let address = address_of_pitch_classes(&[0]).unwrap();
777        assert_eq!(address.cardinality, 1);
778        assert_eq!(address.forte_class, 1);
779        assert_eq!(address.inversion, 0);
780        assert_eq!(address.pitch_class_original, 0);
781        assert!(address_of_pitch_classes(&[]).is_err());
782    }
783}