Horoscope object

The data structures of the six scopes, the three whole-scope lists, plus palace lookups that need not be handed the astrolabe again.

A horoscope projects the natal chart onto a point in time. The same chart shows a different palace layout in different years — which is exactly what "the decadal scope has moved to that palace" means.

let h = chart.horoscope("2025-6-1", 0)?;

HoroscopeRef holds the natal chart that produced it, so none of the query methods need the astrolabe passed in again.

The examples on this page all start from a Language::EnUS natal chart, so the display values in the output are iztro's en-US vocabulary.

HoroscopeData

HoroscopeRef derefs to HoroscopeData, which has eight fields: two date strings and the six scopes.

FieldTypeDescription
solar_dateStringTarget solar date, as passed in
lunar_dateStringThe target date's lunar form, written in Chinese numerals
decadalHoroscopeItemThe decadal scope
ageAgeItemThe age scope
yearlyYearlyItemThe yearly scope
monthlyHoroscopeItemThe monthly scope
dailyHoroscopeItemThe daily scope
hourlyHoroscopeItemThe hourly scope

solar_date is the target date, not the birth date; the birth date lives on the natal chart, as h.astrolabe().solar_date.

The six scopes

FieldTypeSpanDescription
decadalHoroscopeItemTen yearsThe decadal scope; the childhood scope for the years before it begins
ageAgeItemOne yearThe age scope, moving one palace per nominal year
yearlyYearlyItemOne yearThe yearly scope, its palace fixed by the year's pillar
monthlyHoroscopeItemOne monthThe monthly scope
dailyHoroscopeItemOne dayThe daily scope
hourlyHoroscopeItemOne double-hourThe hourly scope

The age scope versus the yearly scope

Both advance once per year, but they start differently: the age scope starts from the birth-year branch and steps forward with the nominal age, while the yearly scope simply asks which palace that year's pillar falls in. The two lines are independent, and Zi Wei practice usually reads them together.

HoroscopeItem

FieldTypeDescription
indexusizeWhich palace this scope lands on (a palace index)
nameStringDisplay name of the scope, translated into the output language
name_keyHoroscopeNameThe scope's language-independent key; before the decadals begin the decadal scope carries Childhood, a different reading semantics from Decadal — predicate on this, never on the translation
heavenly_stemHeavenlyStemStem of the scope, which determines the mutagens it flies
earthly_branchEarthlyBranchBranch of the scope
palace_namesVec<Palace>The twelve palace names re-derived with this scope's palace as the Soul palace, indexed by palace index
mutagenVec<StarKey>The stars this scope's stem transforms, in the order lu, quan, ke, ji
starsOption<Vec<Vec<Star>>>The scope stars of this layer; None for layers that have none

age and yearly are not HoroscopeItems themselves but wrappers carrying one extra datum each:

pub struct AgeItem {
    pub base: HoroscopeItem,
    pub nominal_age: u32,          // the nominal age for that date
}

pub struct YearlyItem {
    pub base: HoroscopeItem,
    pub yearly_dec_star: YearlyDecStar,
}

pub struct YearlyDecStar {
    pub jiangqian12: Vec<StarKey>, // the yearly Jiang-qian gods, indexed by palace index
    pub suiqian12: Vec<StarKey>,   // the yearly Sui-qian gods, indexed by palace index
}

AgeItem and YearlyItem both implement Deref<Target = HoroscopeItem>, so the shared fields read directly: h.yearly.heavenly_stem, h.age.index; take .base when you need the whole HoroscopeItem. All four types are re-exported at the crate root.

Example

let h = chart.horoscope("2025-6-1", 0)?;

for item in [&h.decadal, &h.monthly, &h.daily, &h.hourly] {
    println!("{} lands on palace {} with pillar {} {}", item.name, item.index,
        translate_heavenly_stem(item.heavenly_stem, Language::EnUS),
        translate_earthly_branch(item.earthly_branch, Language::EnUS));
}
println!("age scope nominal age {}", h.age.nominal_age);

Output

decadal lands on palace 2 with pillar geng chen
monthly lands on palace 3 with pillar ren woo
daily lands on palace 8 with pillar xin chou
hourly lands on palace 8 with pillar wu zi
age scope nominal age 26

The three lists

decadal_list / yearly_list / monthly_list are methods on Astrolabe, not on the horoscope object: they return a whole scope in one call, sparing you from looping over dates with horoscope and assembling the result yourself. Each item carries that scope's time coordinates on top of the shared HoroscopeItem:

pub struct DecadalHoroscope {
    pub base: HoroscopeItem,
    pub palace_name: Palace,     // the natal palace this decadal sits in
    pub age_range: (u32, u32),   // first and last nominal age, both inclusive
    pub year_range: (i64, i64),  // first and last lunar year, both inclusive
}

pub struct YearlyHoroscope {
    pub base: HoroscopeItem,
    pub age: u32,                // the nominal age this year corresponds to
    pub year: i64,               // lunar year
}

pub struct MonthlyHoroscope {
    pub base: HoroscopeItem,
    pub age: u32,                // the nominal age this month falls in
    pub year: i64,               // lunar year
    pub month: u32,              // lunar month, 1 for the first; a leap month shares its number
    pub is_leap_month: bool,     // whether this item is the leap month
    pub part: MonthPart,         // Normal whole month / First leap first half / Second leap second half
    pub day_range: (u32, u32),   // the lunar days this segment covers, both inclusive
}

All three implement Deref<Target = HoroscopeItem>, so the shared fields read straight through (d.heavenly_stem, d.palace_names); take .base when you want the whole HoroscopeItem. The four types and MonthPart are all re-exported from the crate root.

The lists use the hour pillar's branch

Every item in the three lists is computed from the hour matching the hour pillar's earthly branch, not the time_index passed at charting time. The two differ only for the late zi hour: on a chart built with index 12, the hour pillar's branch is zi, so the lists use hour index 0.


decadal_list

Purpose Get all twelve decadals of this chart at once, ordered by when they begin.

Zi Wei meaning A decadal is a ten-year step walking the twelve palaces from the starting palace. Laid out flat, the line tells you which palace a given stretch of life falls in and which ten years it covers.

Signature

pub fn decadal_list(&self) -> Vec<DecadalHoroscope>

Return value Always 12 items; index 0 is the first decadal. The order follows the starting nominal age, not the palace index — decadals run forwards or backwards depending on yin/yang and gender.

Example

let en = Language::EnUS;

for d in chart.decadal_list().iter().take(3) {
    println!("{} {} {} {}-{} {}-{}",
        translate_palace(d.palace_name, en),
        translate_heavenly_stem(d.heavenly_stem, en),
        translate_earthly_branch(d.earthly_branch, en),
        d.age_range.0, d.age_range.1, d.year_range.0, d.year_range.1);
}
println!("{}", chart.decadal_list().len());

Output

soul ren woo 3-12 2002-2011
siblings xin si 13-22 2012-2021
spouse geng chen 23-32 2022-2031
12

This chart starts its decadals at nominal age 3, with the first one in the Soul palace.

Edge cases and pitfalls

Childhood is not in the list

The years before the first decadal begins are the childhood scope, which only appears when you query horoscope for a specific date (name_key is Childhood). decadal_list lists the twelve decadals themselves, so every item's name_key is Decadal.


yearly_list

Purpose Get every yearly scope inside one decadal.

Zi Wei meaning Fix the decadal, then read year by year — the usual order in which a reading advances. A decadal spans ten years, and these are those ten.

Signature

pub fn yearly_list(&self, target: impl Into<DecadalTarget>) -> Result<Vec<YearlyHoroscope>, IztroError>

Parameters

ParameterTypeRequiredDefaultDescription
targetimpl Into<DecadalTarget>Yes—The decadal's ordinal (usize, 0 for the first) or the natal palace it sits in (Palace)
pub enum DecadalTarget {
    Ordinal(usize),  // ordinal by starting age
    Name(Palace),    // the natal palace this decadal sits in
}

When both forms address the same decadal they return identical results; pick whichever you already hold.

Return value Result<Vec<YearlyHoroscope>, IztroError> with 10 items ordered by nominal age. An out-of-range ordinal or an unmatched palace name returns Err.

Example

let en = Language::EnUS;
let list = chart.yearly_list(2usize)?;

for y in list.iter().take(3) {
    println!("{} {} {} {} -> {}", y.age, y.year,
        translate_heavenly_stem(y.heavenly_stem, en),
        translate_earthly_branch(y.earthly_branch, en), y.index);
}
println!("{} {}", list.len(), chart.yearly_list(Palace::Spouse)?.len());

Output

23 2022 ren yin -> 0
24 2023 gui mao -> 1
25 2024 jia chen -> 2
10 10

Decadal 2 sits in the spouse palace, so yearly_list(2) and yearly_list(Palace::Spouse) name the same one.

Edge cases and pitfalls

There is no default decadal

One of the two addressing forms is mandatory; there is no "omit it and get the current decadal" behaviour. Silently picking a default would turn a forgotten argument into a plausible-looking wrong answer.


monthly_list

Purpose Get every monthly scope of one lunar year.

Zi Wei meaning The monthly scope sits below the yearly one and advances month by month. How to treat a leap month is a point where schools disagree, so whether to split it is the caller's decision.

Signature

pub fn monthly_list(&self, year: i64, fix_leap: bool) -> Result<Vec<MonthlyHoroscope>, IztroError>

Parameters

ParameterTypeRequiredDefaultDescription
yeari64Yes—The lunar year
fix_leapboolYes—Whether to split a leap month into two half-month items

Return value Result<Vec<MonthlyHoroscope>, IztroError>; the length depends on whether the year has a leap month and on fix_leap:

That lunar yearfix_leapItemsHow the leap month is laid out
No leap montheither12—
Has a leap monthtrue14Split into First (days 1-15) and Second (day 16 to month end)
Has a leap monthfalse13One whole-month item with part set to Normal

The leap month follows the ordinary month of the same number. Each item is computed from the first day of its segment (day 16 for a second half).

Example

let en = Language::EnUS;
let list = chart.monthly_list(2020, true)?;

println!("{}", list.len());
for m in list.iter().skip(3).take(4) {
    println!("{} {} {} {}-{} {} {}", m.month, m.is_leap_month, m.part.as_key(),
        m.day_range.0, m.day_range.1,
        translate_heavenly_stem(m.heavenly_stem, en),
        translate_earthly_branch(m.earthly_branch, en));
}
println!("{} {}", chart.monthly_list(2020, false)?.len(), chart.monthly_list(2021, true)?.len());

Output

14
4 false normal 1-30 xin si
4 true first 1-15 xin si
4 true second 16-29 ren woo
5 false normal 1-30 ren woo
13 12

Lunar 2020 has a leap fourth month. Split apart, its first half shares the pillar of the fourth month (xin si) and its second half shares the fifth month's (ren woo) — which is what "the second half of a leap month counts as the next month" means. Lunar 2021 has no leap month, so it is always 12 items.

Edge cases and pitfalls

This fix_leap is unrelated to the charting fix_leap

The fix_leap on the charting entry decides whether someone born in the second half of a leap month is placed by the next month's stars — it changes the natal layout. This one only decides whether this list splits the leap month. The two may differ and do not affect each other.

A leap month shares its number

The leap fourth month also has month == 4; is_leap_month is what tells them apart. Deduplicating by month drops the leap month.


age_palace

Purpose Get the palace the age scope occupies this year.

Zi Wei meaning The age scope is a line advancing year by year; whichever palace it lands on becomes the focus for that year.

Signature

pub fn age_palace(&self) -> PalaceRef<'a>

Return value PalaceRef — a palace on the natal chart, always present.

Example

let h = chart.horoscope("2025-6-1", 0)?;
println!("{}", translate_palace(h.age_palace().name, Language::EnUS));

Output

property

palace

Purpose Get one of the twelve palaces as re-derived under a given horoscope scope.

Zi Wei meaning Once the decadal scope reaches a palace, the twelve palaces are re-anchored with that palace as the "decadal Soul palace". "The decadal Spouse palace" refers to that re-anchored naming, and it is usually not the same palace as the natal Spouse palace.

Signature

pub fn palace(&self, name: Palace, scope: Scope) -> Option<PalaceRef<'a>>

Parameters

ParameterTypeRequiredDefaultDescription
namePalaceYes—The palace name to fetch
scopeScopeYes—Which scope's twelve palaces to search

Return value Option<PalaceRef<'a>> — a palace on the natal chart (the same cell carries different names under different scopes). With Origin as the scope, these are the natal twelve palaces.

Example

let en = Language::EnUS;
let h = chart.horoscope("2025-6-1", 0)?;

println!("the decadal Soul palace is the natal {}",
    translate_palace(h.palace(Palace::Soul, Scope::Decadal).unwrap().name, en));
println!("the natal Soul palace is {}",
    translate_palace(h.palace(Palace::Soul, Scope::Origin).unwrap().name, en));

Output

the decadal Soul palace is the natal spouse
the natal Soul palace is soul

Edge cases and pitfalls

What comes back is the cell on the natal chart

On the palace object returned by palace(Soul, Decadal), name is still the natal palace name (Spouse in the example), because it is that cell on the natal chart. To see what the cell is called at the decadal layer, read h.decadal.palace_names[index].


surround_palaces

Purpose Get the surrounded palaces of a palace under a given horoscope scope.

Signature

pub fn surround_palaces(&self, name: Palace, scope: Scope) -> Option<SurroundedPalaces<'a>>

Parameters Same as palace.

Return value Option<SurroundedPalaces<'a>>; its predicates are on Surrounded palaces.

Example

let h = chart.horoscope("2025-6-1", 0)?;
let sp = h.surround_palaces(Palace::Wealth, Scope::Yearly).unwrap();

println!("the surrounded set of the yearly Wealth palace is anchored on the natal {}",
    translate_palace(sp.target.name, Language::EnUS));

Output

the surrounded set of the yearly Wealth palace is anchored on the natal health

has_horoscope_stars / has_one_of_horoscope_stars / not_have_horoscope_stars

Purpose Test whether a palace under a given scope holds the given scope stars.

Zi Wei meaning Scope stars are a group produced by each horoscope layer: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. They carry different names in different layers — Yunkui and Yunyue at the decadal layer, Liukui and Liuyue at the yearly layer — with the same meaning applied to their own time span.

Signature

pub fn has_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool
pub fn has_one_of_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool
pub fn not_have_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool

Parameters

ParameterTypeRequiredDefaultDescription
namePalaceYes—The palace name under that scope
scopeScopeYes—The horoscope scope
stars&[StarKey]Yes—Scope star keys, which must use that layer's names

Return value

MethodMeaning
has_horoscope_starsAll of them are present
has_one_of_horoscope_starsAt least one is present
not_have_horoscope_starsNone is present

Example

use x_iztro::StarKey::*;

let h = chart.horoscope("2025-6-1", 0)?;

println!("{}", h.has_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yunlu]));
println!("{}", h.has_one_of_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yunlu, Yunyang]));
println!("{}", h.not_have_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yuntuo]));

Output

false
false
true

Edge cases and pitfalls


has_horoscope_mutagen

Purpose Test whether a palace under a given scope carries a mutagen flown by that scope's stem.

Zi Wei meaning Every horoscope layer has a stem of its own, and it transforms four stars just as the birth-year stem does. A question like "does the decadal lu land in the decadal Wealth palace?" is asking about this.

Signature

pub fn has_horoscope_mutagen(&self, name: Palace, scope: Scope, mutagen: Mutagen) -> bool

Parameters

ParameterTypeRequiredDefaultDescription
namePalaceYes—The palace name under that scope
scopeScopeYes—The horoscope scope
mutagenMutagenYes—One of the four mutagens

Return value bool. It checks whether the star transformed by that layer's stem sits among the target palace's major or minor stars (adjective stars are not considered).

Example

let h = chart.horoscope("2025-6-1", 0)?;

println!("{}", h.has_horoscope_mutagen(Palace::Soul, Scope::Decadal, Mutagen::Lu));

// the four stars this layer transforms can be read directly
println!("{:?}", h.decadal.mutagen.iter()
    .map(|s| translate_star(*s, Language::EnUS)).collect::<Vec<_>>());

Output

false
["sun", "warrior", "moon", "fortunate"]

The decadal stem is geng, and geng sends lu to Taiyang (sun), quan to Wuqu (warrior), ke to Taiyin (moon) and ji to Tiantong (fortunate).

Edge cases and pitfalls

Always false when the scope is Origin

There is no such thing as a "layer stem" at the natal layer — the natal mutagens are already marked on the stars' own mutagen fields. has_horoscope_mutagen(name, Scope::Origin, m) therefore returns false outright, which does not mean the natal chart lacks that mutagen. For natal mutagens use the palace's has_mutagen.


astrolabe / data / into_data

Purpose Get back to the natal chart, or take out the horoscope's plain data.

Signature

pub fn astrolabe(&self) -> &'a Astrolabe
pub fn data(&self) -> &HoroscopeData
pub fn into_data(self) -> HoroscopeData

Return value

MethodUse
astrolabeBack to the natal chart that produced this horoscope
dataBorrow the underlying data; the view implements Deref, so h.decadal usually suffices
into_dataTake the data and drop the borrow of the astrolabe, for cases needing a 'static lifetime

Example

let h = chart.horoscope("2025-6-1", 0)?;

println!("{}", h.astrolabe().solar_date);

let data: HoroscopeData = h.into_data();   // no longer borrows chart
println!("{}", data.solar_date);

Output

2000-8-16
2025-6-1

to_text

Purpose The horoscope's semantic text: a complete description for language models and people, as a Markdown subset.

Signature

pub fn to_text(&self) -> String

Defined on HoroscopeRef and emitting in the chart's charting language; for an explicit language use the free function text::horoscope_to_text(astrolabe, horoscope, lang).

Return value String — one section each for the decadal (childhood before the decadals start), age fortune, yearly, monthly, daily and hourly scopes, each with its mutagens and the patterns and flowing stars from its own perspective; the decadal and yearly sections expand into a twelve-palace table. The full format is on Semantic text.

Example

let h = chart.horoscope("2025-1-1", 0)?;

for line in h.to_text().lines().take(5) {
    println!("{line}");
}

Output

# Horoscope 2025-1-1 (二〇二四年腊月初二)

## Decadal Fortune · Soul Palace: Natal spouse (geng chen)
- Mutagen: sun [A]→Natal children, warrior [B]→Natal wealth, moon [C]→Natal friends, fortunate [D]→Natal health
- Patterns: Marshal, Rebel and Wolf (soul), Meeting of Wind and Cloud (soul)

to_text_with

Purpose The same text as to_text, with readings attached per TextOptions: each scope's table or facts is followed by readings of that scope's flowing stars and of the patterns hit from its perspective, deduplicated across scopes. Natal star readings are not repeated here — they are in the chart's to_text_with.

Signature

pub fn to_text_with(&self, opts: &TextOptions) -> String

Parameters

ParameterTypeRequiredDefaultDescription
opts&TextOptionsyes—Output options; TextOptions::new().knowledge(pack) adds readings, TextOptions::default() equals to_text

Defined on HoroscopeRef and emitting in the chart's charting language; for an explicit language use the free function text::horoscope_to_text_with(astrolabe, horoscope, opts, lang). Placement is on Text with readings.

Example (on a zh-CN chart, since the bundled pack is Chinese)

let pack = KnowledgePack::builtin(Language::ZhCN).unwrap();
let h = chart.horoscope("2025-1-1", 0)?;
let text = h.to_text_with(&TextOptions::new().knowledge(pack));

println!("{} {}", h.to_text().chars().count(), text.chars().count());
for line in text.lines().filter(|l| l.starts_with("## ")) {
    println!("{line}");
}

Output

2457 8458
## 大限 · 命宫: 本命夫妻 (庚辰)
## 小限 · 命宫: 本命官禄 · 虚岁 25
## 流年 · 命宫: 本命夫妻 (甲辰)
## 流月 · 命宫: 本命仆役 (丁丑)
## 流日 · 命宫: 本命迁移 (庚午)
## 流时 · 命宫: 本命迁移 (丙子)

to_dto

Purpose Convert the horoscope data into the serialization structure that matches the JS iztro field contract.

Signature

pub fn to_dto(&self, lang: Language) -> HoroscopeDto

Parameters

ParameterTypeRequiredDefaultDescription
langLanguageYes—Which language the translated fields use

It is defined on HoroscopeData (not on HoroscopeRef). The horoscope data does not itself record a language, so it has to be given explicitly here — usually chart.language, to stay consistent with the natal chart.

Return value x_iztro::dto::HoroscopeDto, camelCase keys plus the *Key identifiers.

Example

let h = chart.horoscope("2025-6-1", 0)?;
let json = serde_json::to_string(&h.to_dto(chart.language))?;
let v: serde_json::Value = serde_json::from_str(&json)?;

println!("{} {}", v["solarDate"], v["decadal"]["heavenlyStem"]);
println!("{}", v["age"]["nominalAge"]);

Output

"2025-6-1" "geng"
26

On this page