Extending the astrolabe

Adding your own analysis methods to Astrolabe and PalaceRef with extension traits.

Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. x-iztro's answer is to let you attach your own rules as methods on the astrolabe — the call syntax matches the built-in methods, and it all happens at compile time, so it is type-checked and costs nothing at runtime.

The recipe

Define a trait declaring the methods you want to add
Implement it for Astrolabe (or PalaceRef, or StarRef)
use the trait where you call it, and the methods are available
use x_iztro::i18n::translate_star;
use x_iztro::*;

/// Adds two custom analysis methods to the astrolabe.
trait MyAnalysis {
    /// Major stars of the Soul palace (borrowing the opposite palace when empty), comma separated
    fn major_star(&self) -> String;
    /// The number of the five elements class
    fn five_elements_value(&self) -> usize;
}

impl MyAnalysis for Astrolabe {
    fn major_star(&self) -> String {
        let soul = self.palace(Palace::Soul).expect("the Soul palace always exists");
        let source = if soul.is_empty() { soul.opposite_palace() } else { soul };

        source
            .major_stars
            .iter()
            .filter(|s| s.star_type == StarType::Major)
            .map(|s| translate_star(s.key, self.language))
            .collect::<Vec<_>>()
            .join(",")
    }

    fn five_elements_value(&self) -> usize {
        self.five_elements_class.value()
    }
}

Usage

let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;

println!("{}", chart.major_star());
println!("{}", chart.five_elements_value());

// the extension method follows the charting language
let zh = by_solar("2000-8-16", 2, Gender::Female, true, Language::ZhCN, Config::default())?;
println!("{}", zh.major_star());

Output

emperor
3
紫微

Extending other types

The same recipe works for palaces and stars. Mind the lifetime parameter when implementing for a view type:

trait PalaceAnalysis {
    /// Whether this palace is "afflicted": holds a malefic and carries ji
    fn is_afflicted(&self) -> bool;
}

impl PalaceAnalysis for PalaceRef<'_> {
    fn is_afflicted(&self) -> bool {
        use x_iztro::StarKey::*;
        self.has_one_of(&[QingyangMin, TuoluoMin, HuoxingMin, LingxingMin, DikongMin, DijieMin])
            && self.has_mutagen(Mutagen::Ji)
    }
}
for palace in &chart.palaces {
    let p = chart.palace(palace.index).unwrap();
    if p.is_afflicted() {
        println!("{} is afflicted", translate_palace(p.name, Language::EnUS));
    }
}

Output

health is afflicted

How to organize this


Compared to runtime injection

Some libraries let you attach functions to objects at runtime. Rust's extension traits do the same thing at compile time; the differences are:

Extension traitRuntime injection
Whether the method existsKnown at compile timeKnown only at runtime
Type checkingYesNo
Call costSame as a built-in methodOne extra dynamic lookup
When errors appearCompilation failsFails at runtime
ScopeVisible only where usedGlobal or per instance

The price is that extension methods must be written at compile time and cannot be decided by a config file or user input. When you need that flexibility, dispatch yourself through a HashMap<String, fn(&Astrolabe) -> bool>.

On this page