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    // Compared without writing the classification out in lower case: a
312    // chord is named through here and there is only the one to match.
313    let inversion_suffix = if classification.eq_ignore_ascii_case("tn") {
314        match inversion {
315            Sign::NegativeOne => "B",
316            Sign::One => "A",
317            Sign::Zero => "",
318        }
319    } else {
320        ""
321    };
322    Ok(format!("{card}-{index}{inversion_suffix}"))
323}
324
325/// The prime form of a set class, which is the same whichever inversion the
326/// address names.
327pub(crate) fn prime_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
328    let (card, index, inversion) = validate_address((address.0, address.1, None))?;
329    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
330        Error::ChordTables(format!(
331            "cannot resolve prime form for address ({card}, {index})"
332        ))
333    })?;
334    Ok(bool_vec_to_pitch_classes(&entry.0))
335}
336
337/// The transposed normal form for a cardinality and Forte number, with the
338/// inversion left to the table's default when `None`.
339pub(crate) fn transposed_normal_form(
340    cardinality: u8,
341    index: u8,
342    inversion: Option<i8>,
343) -> Result<Vec<u8>, Error> {
344    let (card, index, inversion) = validate_address((cardinality, index, inversion))?;
345    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
346        Error::ChordTables(format!(
347            "cannot resolve normal form for address ({card}, {index}, {})",
348            inversion.as_i8()
349        ))
350    })?;
351    Ok(bool_vec_to_pitch_classes(&entry.0))
352}
353
354pub(crate) fn transposed_normal_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
355    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
356    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
357        Error::ChordTables(format!(
358            "cannot resolve normal form for address ({card}, {index}, {})",
359            inversion.as_i8()
360        ))
361    })?;
362    Ok(bool_vec_to_pitch_classes(&entry.0))
363}
364
365pub(crate) fn interval_class_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
366    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
367    let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
368        Error::ChordTables(format!(
369            "cannot resolve interval class vector for address ({card}, {index}, {})",
370            inversion.as_i8()
371        ))
372    })?;
373    Ok(entry.2.to_vec())
374}
375
376pub(crate) fn invariance_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
377    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
378    let entry = FORTE[card as usize]
379        .get(index as usize)
380        .and_then(Option::as_ref)
381        .ok_or_else(|| {
382            Error::ChordTables(format!(
383                "cannot resolve invariance vector for address ({card}, {index}, {})",
384                inversion.as_i8()
385            ))
386        })?;
387    Ok(entry.invariance_vector().to_vec())
388}
389
390pub(crate) fn z_relation_from_address(address: RawAddress) -> Result<Option<String>, Error> {
391    let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
392    let entry = FORTE[card as usize]
393        .get(index as usize)
394        .and_then(Option::as_ref)
395        .ok_or_else(|| {
396            Error::ChordTables(format!(
397                "cannot resolve z-relation for address ({card}, {index}, {})",
398                inversion.as_i8()
399            ))
400        })?;
401    let z_relation = entry.z_relation();
402    if z_relation == 0 {
403        Ok(None)
404    } else {
405        Ok(Some(format!("{card}-{}", z_relation)))
406    }
407}
408
409pub(crate) fn known_chord_table_entries() -> Vec<KnownChordTableEntry> {
410    let mut entries = TN_INDEX_TO_CHORD_INFO
411        .iter()
412        .filter_map(|&((cardinality, index, inversion), common_names)| {
413            let common_names = common_names.unwrap_or_default().to_vec();
414            let address = (cardinality, index, inversion.as_i8(), None);
415            Some((
416                (cardinality, index, inversion.as_i8()),
417                KnownChordTableEntry {
418                    cardinality,
419                    common_names,
420                    forte_class: address_to_forte_name(address, "tn").ok()?,
421                    normal_form: transposed_normal_form_from_address(address).ok()?,
422                    interval_class_vector: interval_class_vector_from_address(address).ok()?,
423                },
424            ))
425        })
426        .collect::<Vec<_>>();
427
428    entries.sort_by_key(|(sort_key, _)| *sort_key);
429    entries.into_iter().map(|(_, entry)| entry).collect()
430}
431
432fn find_cardinality_member(card: u8, index: u8, inversion: Sign) -> Option<&'static Pcivicv> {
433    CARDINALITY_TO_CHORD_MEMBERS
434        .get(card as usize)?
435        .iter()
436        .find_map(|(key, value)| (*key == (index, inversion)).then_some(value))
437}
438
439fn find_tn_index_to_chord_info(
440    card: u8,
441    index: u8,
442    inversion: Sign,
443) -> Option<&'static [&'static str]> {
444    TN_INDEX_TO_CHORD_INFO
445        .iter()
446        .find_map(|(key, names)| (*key == (card, index, inversion)).then_some(*names))
447        .flatten()
448}
449
450// include!("./generated.rs");
451
452// ---------------------------------------------------------------------------
453// The tables as music21 exposes them: a set class named by its cardinality
454// and Forte class number, asked about without a chord in hand.
455// ---------------------------------------------------------------------------
456
457/// The inversions a set class comes in: `[0]` for one that is its own
458/// inversion, and `[-1, 1]` for the two halves of an inversionally related
459/// pair.
460///
461/// music21's `forteIndexToInversionsAvailable`.
462///
463/// # Errors
464///
465/// When the cardinality or the class number names no set class.
466pub fn inversions_available(cardinality: u8, forte_class: u8) -> Result<Vec<i8>, Error> {
467    Ok(
468        forte_index_to_inversions_available(cardinality as usize, forte_class)?
469            .into_iter()
470            .map(|sign| sign.as_i8())
471            .collect(),
472    )
473}
474
475/// Reads an address, filling in the inversion where one was not given:
476/// music21's `_validateAddress`.
477///
478/// A class that is its own inversion answers nought; one that is not answers
479/// the first of its pair.
480///
481/// # Errors
482///
483/// When the cardinality, the class number or the inversion names no set
484/// class.
485pub fn read_address(
486    cardinality: u8,
487    forte_class: u8,
488    inversion: Option<i8>,
489) -> Result<(u8, u8, i8), Error> {
490    let (cardinality, forte_class, inversion) =
491        validate_address((cardinality, forte_class, inversion))?;
492    Ok((cardinality, forte_class, inversion.as_i8()))
493}
494
495/// The set class's normal form, transposed to start on zero: music21's
496/// `addressToTransposedNormalForm`.
497///
498/// # Errors
499///
500/// When the address names no set class.
501pub fn normal_form(
502    cardinality: u8,
503    forte_class: u8,
504    inversion: Option<i8>,
505) -> Result<Vec<u8>, Error> {
506    transposed_normal_form(cardinality, forte_class, inversion)
507}
508
509/// The set class's prime form, which is the same whichever inversion the
510/// address names: music21's `addressToPrimeForm`.
511///
512/// # Errors
513///
514/// When the address names no set class.
515pub fn prime_form(cardinality: u8, forte_class: u8) -> Result<Vec<u8>, Error> {
516    transposed_normal_form(cardinality, forte_class, None)
517}
518
519/// The set class's interval class vector, six counts of how many intervals
520/// of each class it holds: music21's `addressToIntervalVector`.
521///
522/// # Errors
523///
524/// When the address names no set class.
525pub fn interval_class_vector(
526    cardinality: u8,
527    forte_class: u8,
528    inversion: Option<i8>,
529) -> Result<Vec<u8>, Error> {
530    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
531    interval_class_vector_from_address((cardinality, forte_class, inversion, None))
532}
533
534/// Every set class with this interval vector, as cardinality and Forte class
535/// number pairs: music21's `intervalVectorToAddress`.
536///
537/// More than one comes back for a Z-related pair — the all-interval
538/// tetrachord is both 4-15 and 4-29 — and none at all for a vector no set
539/// class has. The inversion is not part of the answer, since a vector says
540/// nothing about which half of a pair is meant.
541///
542/// # Errors
543///
544/// When the vector is not six counts long.
545pub fn set_classes_with_interval_vector(vector: &[u8]) -> Result<Vec<(u8, u8)>, Error> {
546    if vector.len() != 6 {
547        return Err(Error::ChordTables(
548            "Vector must have exactly six entries".to_string(),
549        ));
550    }
551    let mut found = Vec::new();
552    for (cardinality, classes) in FORTE.iter().enumerate().skip(1) {
553        for (forte_class, entry) in classes.iter().enumerate() {
554            let Some(entry) = entry else {
555                continue;
556            };
557            if entry.1 == vector {
558                found.push((cardinality as u8, forte_class as u8));
559            }
560        }
561    }
562    Ok(found)
563}
564
565/// The set class Z-related to this one — the other with the same interval
566/// vector — where there is one: music21's `addressToZAddress`.
567///
568/// The inversion comes back read the way [`read_address`] reads one.
569///
570/// # Errors
571///
572/// When the address names no set class.
573pub fn z_related_class(cardinality: u8, forte_class: u8) -> Result<Option<(u8, u8, i8)>, Error> {
574    let (cardinality, forte_class, _) = read_address(cardinality, forte_class, None)?;
575    let related = FORTE[cardinality as usize]
576        .get(forte_class as usize)
577        .and_then(Option::as_ref)
578        .map_or(0, TNITupleExt::z_relation);
579    if related == 0 {
580        return Ok(None);
581    }
582    Some(read_address(cardinality, related, None)).transpose()
583}
584
585/// What the set class is commonly called, where it is called anything:
586/// music21's `addressToCommonNames`.
587///
588/// More than one name comes back where more than one is in use, and nothing
589/// where the class has no common name. The names take no account of spelling
590/// or inversion, so a minor triad answers "major triad"; [`Chord::common_name`]
591/// is the one that reads the chord as it is written.
592///
593/// # Errors
594///
595/// When the address names no set class.
596///
597/// [`Chord::common_name`]: crate::Chord::common_name
598pub fn common_names(
599    cardinality: u8,
600    forte_class: u8,
601    inversion: Option<i8>,
602) -> Result<Option<Vec<&'static str>>, Error> {
603    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
604    address_to_common_names((cardinality, forte_class, inversion, None))
605}
606
607/// The set class's Forte name, such as `8-15B`: music21's
608/// `addressToForteName`.
609///
610/// `inversional_equivalence` is music21's `'tni'` classification, which
611/// leaves off the `A` and `B` that tell the two halves of a pair apart.
612///
613/// # Errors
614///
615/// When the address names no set class.
616pub fn forte_name(
617    cardinality: u8,
618    forte_class: u8,
619    inversion: Option<i8>,
620    inversional_equivalence: bool,
621) -> Result<String, Error> {
622    let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
623    address_to_forte_name(
624        (cardinality, forte_class, inversion, None),
625        if inversional_equivalence { "tni" } else { "tn" },
626    )
627}
628
629/// Where a set of pitch classes sits in the tables: music21's
630/// `seekChordTablesAddress`.
631///
632/// The pitch classes are the chord's own, in order and without repeats —
633/// [`Chord::pitch_classes`] is what a chord hands over.
634///
635/// # Errors
636///
637/// When there are no pitch classes at all, or more than the tables hold.
638///
639/// [`Chord::pitch_classes`]: crate::Chord::pitch_classes
640pub fn address_of_pitch_classes(pitch_classes: &[u8]) -> Result<ChordTableAddress, Error> {
641    let (cardinality, forte_class, inversion, original) = seek_chord_tables_address(pitch_classes)?;
642    Ok(ChordTableAddress {
643        cardinality,
644        forte_class,
645        inversion,
646        pitch_class_original: original.unwrap_or(0),
647    })
648}
649
650#[cfg(test)]
651mod tests {
652
653    #[test]
654    fn the_address_helpers_read_the_tables_for_a_major_triad() {
655        use super::{
656            interval_class_vector_from_address, invariance_vector_from_address,
657            prime_form_from_address, transposed_normal_form_from_address, z_relation_from_address,
658        };
659
660        let major = (3, 11, -1, Some(0));
661        assert_eq!(prime_form_from_address(major).unwrap(), [0, 3, 7]);
662        assert_eq!(
663            transposed_normal_form_from_address(major).unwrap(),
664            [0, 4, 7]
665        );
666        assert_eq!(
667            interval_class_vector_from_address(major).unwrap(),
668            [0, 0, 1, 1, 1, 0]
669        );
670        assert_eq!(invariance_vector_from_address(major).unwrap().len(), 8);
671        assert_eq!(z_relation_from_address(major).unwrap(), None);
672        assert_eq!(
673            z_relation_from_address((4, 15, 1, Some(0)))
674                .unwrap()
675                .as_deref(),
676            Some("4-29")
677        );
678        assert!(prime_form_from_address((3, 99, 0, None)).is_err());
679    }
680
681    use super::{Sign, find_cardinality_member};
682
683    #[test]
684    fn cardinality_to_chord_members_include_major_triad() {
685        let member = find_cardinality_member(3, 11, Sign::NegativeOne).unwrap();
686        assert_eq!(
687            member.0,
688            [
689                true, false, false, false, true, false, false, true, false, false, false, false
690            ]
691        );
692        assert_eq!(member.2, [0, 0, 1, 1, 1, 0]);
693    }
694
695    /// music21's own answers, out of the docstrings of `chord/tables.py`.
696    #[test]
697    fn the_tables_answer_what_music21_answers() {
698        use super::*;
699
700        assert_eq!(inversions_available(3, 1).unwrap(), [0]);
701        assert_eq!(inversions_available(3, 2).unwrap(), [-1, 1]);
702        assert_eq!(inversions_available(3, 12).unwrap(), [0]);
703        assert!(inversions_available(20, 1).is_err());
704        assert!(inversions_available(8, 200).is_err());
705
706        // An inversion left out is read off the table: nought where the
707        // class is its own inversion, and the first of the pair otherwise.
708        assert_eq!(read_address(3, 1, Some(0)).unwrap(), (3, 1, 0));
709        assert_eq!(read_address(2, 3, None).unwrap(), (2, 3, 0));
710        assert_eq!(read_address(3, 12, None).unwrap(), (3, 12, 0));
711        assert!(read_address(8, 3, Some(-30)).is_err());
712
713        assert_eq!(normal_form(3, 1, Some(0)).unwrap(), [0, 1, 2]);
714        assert_eq!(normal_form(3, 11, Some(-1)).unwrap(), [0, 4, 7]);
715        assert_eq!(normal_form(3, 11, Some(1)).unwrap(), [0, 3, 7]);
716        assert_eq!(normal_form(3, 11, None).unwrap(), [0, 3, 7]);
717
718        // The prime form is the same whichever half of the pair is named.
719        assert_eq!(prime_form(3, 11).unwrap(), [0, 3, 7]);
720        assert_eq!(prime_form(3, 1).unwrap(), [0, 1, 2]);
721
722        assert_eq!(
723            interval_class_vector(3, 1, Some(0)).unwrap(),
724            [2, 1, 0, 0, 0, 0]
725        );
726        assert_eq!(
727            interval_class_vector(3, 11, Some(-1)).unwrap(),
728            [0, 0, 1, 1, 1, 0]
729        );
730        assert_eq!(
731            interval_class_vector(4, 29, None).unwrap(),
732            [1, 1, 1, 1, 1, 1]
733        );
734
735        assert_eq!(
736            set_classes_with_interval_vector(&[7, 6, 5, 4, 4, 2]).unwrap(),
737            [(8, 1)]
738        );
739        assert_eq!(
740            set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 1]).unwrap(),
741            [(5, 10)]
742        );
743        assert!(
744            set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 99])
745                .unwrap()
746                .is_empty()
747        );
748        // The all-interval tetrachord is a Z-related pair, so both come back.
749        assert_eq!(
750            set_classes_with_interval_vector(&[1, 1, 1, 1, 1, 1]).unwrap(),
751            [(4, 15), (4, 29)]
752        );
753        assert!(set_classes_with_interval_vector(&[0, 2, 4]).is_err());
754
755        assert_eq!(z_related_class(5, 12).unwrap(), Some((5, 36, 1)));
756        assert_eq!(z_related_class(5, 36).unwrap(), Some((5, 12, 0)));
757        assert_eq!(z_related_class(3, 11).unwrap(), None);
758        assert_eq!(z_related_class(8, 29).unwrap(), Some((8, 15, 1)));
759
760        assert_eq!(
761            common_names(3, 1, Some(0)).unwrap(),
762            Some(vec!["chromatic trimirror"])
763        );
764        assert_eq!(
765            common_names(3, 11, Some(-1)).unwrap(),
766            Some(vec!["major triad"])
767        );
768        assert_eq!(
769            common_names(7, 33, None).unwrap(),
770            Some(vec!["Neapolitan-major mode", "leading-whole-tone mode"])
771        );
772
773        assert_eq!(forte_name(8, 15, Some(-1), false).unwrap(), "8-15B");
774        assert_eq!(forte_name(8, 15, None, false).unwrap(), "8-15A");
775        assert_eq!(forte_name(3, 12, None, false).unwrap(), "3-12");
776        assert_eq!(forte_name(8, 15, None, true).unwrap(), "8-15");
777        assert_eq!(forte_name(5, 37, None, false).unwrap(), "5-37");
778
779        let address = address_of_pitch_classes(&[0]).unwrap();
780        assert_eq!(address.cardinality, 1);
781        assert_eq!(address.forte_class, 1);
782        assert_eq!(address.inversion, 0);
783        assert_eq!(address.pitch_class_original, 0);
784        assert!(address_of_pitch_classes(&[]).is_err());
785    }
786}