Extending the astrolabe
Adding custom analysis methods to a chart through struct embedding.
Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. Go
forbids adding methods to a type from another package, so the extension point is embedding: put
*Astrolabe inside a struct of your own, and the new methods are called with a dot just like the
built-in ones — and checked at compile time.
The recipe
*iztro.Astrolabepackage main
import (
"strings"
"github.com/x-haose/x-iztro/go/iztro"
)
// MyChart embeds the astrolabe and adds analysis methods of its own.
type MyChart struct {
*iztro.Astrolabe
}
// MajorStar returns the major stars of the Soul palace (borrowing the opposite
// palace when empty), comma separated.
func (c MyChart) MajorStar() string {
soul := c.Palace(iztro.PalaceSoul)
source := soul
if soul.IsEmpty() {
source = soul.OppositePalace()
}
names := make([]string, 0, len(source.MajorStars))
for _, s := range source.MajorStars {
if s.Type == iztro.StarTypeMajor {
names = append(names, s.Name)
}
}
return strings.Join(names, ",")
}
// FiveElementsValue returns the number of the five elements class.
func (c MyChart) FiveElementsValue() int {
return map[string]int{
iztro.ClassWater2nd: 2,
iztro.ClassWood3rd: 3,
iztro.ClassMetal4th: 4,
iztro.ClassEarth5th: 5,
iztro.ClassFire6th: 6,
}[c.FiveElementsClassKey]
}Usage
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil)
my := MyChart{chart}
fmt.Println(my.MajorStar())
fmt.Println(my.FiveElementsValue())
// the built-in fields and methods remain available
fmt.Println(my.SolarDate)
fmt.Println(my.Palace(iztro.PalaceSoul).Name)
// the extension method follows the charting language
zhChart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
fmt.Println(MyChart{zhChart}.MajorStar())Output
emperor
3
2000-8-16
soul
紫微Embed, do not wrap
Write *iztro.Astrolabe, not Astrolabe *iztro.Astrolabe — the former promotes the built-in fields
and methods to the outer type so my.SolarDate works, while the latter forces
my.Astrolabe.SolarDate every time.
Extending other types
The same recipe works for palaces and stars:
type MyPalace struct {
*iztro.Palace
}
// IsAfflicted reports whether this palace holds a malefic and carries ji.
func (p MyPalace) IsAfflicted() bool {
return p.HasOneOf(
iztro.StarQingyangMin, iztro.StarTuoluoMin,
iztro.StarHuoxingMin, iztro.StarLingxingMin,
iztro.StarDikongMin, iztro.StarDijieMin,
) && p.HasMutagen(iztro.MutagenJi)
}for i := range chart.Palaces {
p := MyPalace{&chart.Palaces[i]}
if p.IsAfflicted() {
fmt.Println(p.Name, "is afflicted")
}
}Output
health is afflictedConstraining extensions with an interface
When several chart types must provide the same set of analysis capabilities, use an interface:
type WealthAnalyzer interface {
WealthScore() int
HasWealthPattern() bool
}
func report(a WealthAnalyzer) {
fmt.Println(a.WealthScore(), a.HasWealthPattern())
}Any type implementing both methods can be passed in, checked at compile time.
How to organize this
Compared to runtime injection
Embedding happens at compile time. Against attaching functions to objects at runtime:
| Embedding | Runtime injection | |
|---|---|---|
| Whether the method exists | Known at compile time | Known only at runtime |
| Type checking | Yes | No |
| Call cost | Same as a built-in method | One extra dynamic lookup |
| When errors appear | Compilation fails | Fails at runtime |
| Scope | Affects only your own type | Global 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
map[string]func(*iztro.Astrolabe) bool.