use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum YinYang {
Yang,
Yin,
}
impl YinYang {
pub fn as_str(self) -> &'static str {
match self {
YinYang::Yang => "阳",
YinYang::Yin => "阴",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FiveElements {
Wood,
Metal,
Water,
Fire,
Earth,
}
impl FiveElements {
pub fn as_str(self) -> &'static str {
match self {
FiveElements::Wood => "木",
FiveElements::Metal => "金",
FiveElements::Water => "水",
FiveElements::Fire => "火",
FiveElements::Earth => "土",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(usize)]
pub enum HeavenlyStem {
Jia,
Yi,
Bing,
Ding,
Wu,
Ji,
Geng,
Xin,
Ren,
Gui,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(usize)]
pub enum EarthlyBranch {
Zi,
Chou,
Yin,
Mao,
Chen,
Si,
Wu,
Wei,
Shen,
You,
Xu,
Hai,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(usize)]
pub enum Palace {
Soul,
Parents,
Spirit,
Property,
Career,
Friends,
Surface,
Health,
Wealth,
Children,
Spouse,
Siblings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum FiveElementsClass {
Water2nd = 2,
Wood3rd = 3,
Metal4th = 4,
Earth5th = 5,
Fire6th = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mutagen {
Lu,
Quan,
Ke,
Ji,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Brightness {
Miao,
Wang,
De,
Li,
Ping,
Bu,
Xian,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StarType {
Major,
Soft,
Tough,
Adjective,
Flower,
Helper,
Lucun,
Tianma,
}
impl StarType {
pub fn as_key(self) -> &'static str {
match self {
StarType::Major => "major",
StarType::Soft => "soft",
StarType::Tough => "tough",
StarType::Adjective => "adjective",
StarType::Flower => "flower",
StarType::Helper => "helper",
StarType::Lucun => "lucun",
StarType::Tianma => "tianma",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Scope {
Origin,
Decadal,
Yearly,
Monthly,
Daily,
Hourly,
}
impl Scope {
pub fn as_key(self) -> &'static str {
match self {
Scope::Origin => "origin",
Scope::Decadal => "decadal",
Scope::Yearly => "yearly",
Scope::Monthly => "monthly",
Scope::Daily => "daily",
Scope::Hourly => "hourly",
}
}
pub fn from_key(key: &str) -> Option<Self> {
match key {
"origin" => Some(Scope::Origin),
"decadal" => Some(Scope::Decadal),
"yearly" => Some(Scope::Yearly),
"monthly" => Some(Scope::Monthly),
"daily" => Some(Scope::Daily),
"hourly" => Some(Scope::Hourly),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HoroscopeName {
Decadal,
Childhood,
Age,
Yearly,
Monthly,
Daily,
Hourly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Gender {
Male,
Female,
}
impl Gender {
pub fn yin_yang(self) -> YinYang {
match self {
Gender::Male => YinYang::Yang,
Gender::Female => YinYang::Yin,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Language {
ZhCN,
ZhTW,
EnUS,
JaJP,
KoKR,
ViVN,
}
impl Language {
pub fn as_code(self) -> &'static str {
match self {
Language::ZhCN => "zh-CN",
Language::ZhTW => "zh-TW",
Language::EnUS => "en-US",
Language::JaJP => "ja-JP",
Language::KoKR => "ko-KR",
Language::ViVN => "vi-VN",
}
}
pub fn from_code(code: &str) -> Option<Self> {
match code.to_ascii_lowercase().replace('_', "-").as_str() {
"zh-cn" => Some(Language::ZhCN),
"zh-tw" => Some(Language::ZhTW),
"en-us" => Some(Language::EnUS),
"ja-jp" => Some(Language::JaJP),
"ko-kr" => Some(Language::KoKR),
"vi-vn" => Some(Language::ViVN),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Algorithm {
Default,
Zhongzhou,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AstroType {
Heaven,
Earth,
Human,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LeapMonth {
NotLeap,
Leap,
LeapFixed,
}
impl LeapMonth {
pub fn from_flags(is_leap_month: bool, fix_leap: bool) -> Self {
match (is_leap_month, fix_leap) {
(false, _) => LeapMonth::NotLeap,
(true, false) => LeapMonth::Leap,
(true, true) => LeapMonth::LeapFixed,
}
}
pub fn is_leap_month(self) -> bool {
!matches!(self, LeapMonth::NotLeap)
}
pub fn fix_leap(self) -> bool {
matches!(self, LeapMonth::LeapFixed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum YearDivide {
Normal,
Exact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HoroscopeDivide {
Normal,
Exact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgeDivide {
Normal,
Birthday,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DayDivide {
Forward,
Current,
}
macro_rules! config_switch_keys {
($ty:ident { $($variant:ident => $key:literal),+ $(,)? }) => {
impl $ty {
pub fn as_key(self) -> &'static str {
match self {
$($ty::$variant => $key),+
}
}
pub fn from_key(key: &str) -> Option<Self> {
match key {
$($key => Some($ty::$variant),)+
_ => None,
}
}
}
};
}
config_switch_keys!(YearDivide { Normal => "normal", Exact => "exact" });
config_switch_keys!(HoroscopeDivide { Normal => "normal", Exact => "exact" });
config_switch_keys!(AgeDivide { Normal => "normal", Birthday => "birthday" });
config_switch_keys!(DayDivide { Forward => "forward", Current => "current" });
config_switch_keys!(Algorithm { Default => "default", Zhongzhou => "zhongzhou" });
config_switch_keys!(AstroType { Heaven => "heaven", Earth => "earth", Human => "human" });
config_switch_keys!(LeapMonth { NotLeap => "notLeap", Leap => "leap", LeapFixed => "leapFixed" });
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TableOverrides {
mutagens: std::collections::HashMap<HeavenlyStem, [crate::data::stars::StarKey; 4]>,
brightness: std::collections::HashMap<crate::data::stars::StarKey, [Option<Brightness>; 12]>,
}
impl TableOverrides {
pub fn set_mutagens(&mut self, stem: HeavenlyStem, stars: [crate::data::stars::StarKey; 4]) {
self.mutagens.insert(stem, stars);
}
pub fn set_brightness(
&mut self,
star: crate::data::stars::StarKey,
table: [Option<Brightness>; 12],
) {
self.brightness.insert(star, table);
}
pub fn mutagens_of(&self, stem: HeavenlyStem) -> Option<&[crate::data::stars::StarKey; 4]> {
self.mutagens.get(&stem)
}
pub fn brightness_of(
&self,
star: crate::data::stars::StarKey,
) -> Option<&[Option<Brightness>; 12]> {
self.brightness.get(&star)
}
pub fn is_empty(&self) -> bool {
self.mutagens.is_empty() && self.brightness.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config {
pub year_divide: YearDivide,
pub horoscope_divide: HoroscopeDivide,
pub age_divide: AgeDivide,
pub day_divide: DayDivide,
pub algorithm: Algorithm,
pub astro_type: AstroType,
#[serde(skip)]
pub overrides: Option<std::sync::Arc<TableOverrides>>,
}
impl Default for Config {
fn default() -> Self {
Config {
year_divide: YearDivide::Normal,
horoscope_divide: HoroscopeDivide::Normal,
age_divide: AgeDivide::Normal,
day_divide: DayDivide::Forward,
algorithm: Algorithm::Default,
astro_type: AstroType::Heaven,
overrides: None,
}
}
}
impl Config {
pub fn with_astro_type(mut self, astro_type: AstroType) -> Self {
self.astro_type = astro_type;
self
}
pub fn with_mutagens(
mut self,
stem: HeavenlyStem,
stars: [crate::data::stars::StarKey; 4],
) -> Self {
let mut tables = self
.overrides
.take()
.map_or_else(TableOverrides::default, |arc| {
std::sync::Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
});
tables.set_mutagens(stem, stars);
self.overrides = Some(std::sync::Arc::new(tables));
self
}
pub fn with_brightness(
mut self,
star: crate::data::stars::StarKey,
table: [Option<Brightness>; 12],
) -> Self {
let mut tables = self
.overrides
.take()
.map_or_else(TableOverrides::default, |arc| {
std::sync::Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
});
tables.set_brightness(star, table);
self.overrides = Some(std::sync::Arc::new(tables));
self
}
pub fn mutagens_of(&self, stem: HeavenlyStem) -> [crate::data::stars::StarKey; 4] {
self.overrides
.as_ref()
.and_then(|t| t.mutagens_of(stem))
.copied()
.unwrap_or_else(|| crate::data::heavenly_stems::get_heavenly_stem_info(stem).mutagen)
}
pub fn brightness_of(
&self,
star: crate::data::stars::StarKey,
palace_index: usize,
) -> Option<Brightness> {
let index = palace_index % 12;
if let Some(table) = self.overrides.as_ref().and_then(|t| t.brightness_of(star)) {
return table[index];
}
crate::data::stars::get_brightness_table(star)?[index]
}
}
pub type TimeIndex = u8;
impl HeavenlyStem {
pub fn index(&self) -> usize {
*self as usize
}
pub fn from_index(i: usize) -> Self {
crate::data::constants::HEAVENLY_STEMS[i % 10]
}
}
impl EarthlyBranch {
pub fn index(&self) -> usize {
*self as usize
}
pub fn from_index(i: usize) -> Self {
crate::data::constants::EARTHLY_BRANCHES[i % 12]
}
}
impl Palace {
pub fn index(&self) -> usize {
*self as usize
}
pub fn from_index(i: usize) -> Self {
crate::data::constants::PALACES[i % 12]
}
}
impl FiveElementsClass {
pub fn value(&self) -> usize {
*self as usize
}
}
impl Palace {
pub fn as_key(&self) -> &'static str {
match self {
Palace::Soul => "soulPalace",
Palace::Parents => "parentsPalace",
Palace::Spirit => "spiritPalace",
Palace::Property => "propertyPalace",
Palace::Career => "careerPalace",
Palace::Friends => "friendsPalace",
Palace::Surface => "surfacePalace",
Palace::Health => "healthPalace",
Palace::Wealth => "wealthPalace",
Palace::Children => "childrenPalace",
Palace::Spouse => "spousePalace",
Palace::Siblings => "siblingsPalace",
}
}
}
impl HeavenlyStem {
pub fn as_key(&self) -> &'static str {
match self {
HeavenlyStem::Jia => "jiaHeavenly",
HeavenlyStem::Yi => "yiHeavenly",
HeavenlyStem::Bing => "bingHeavenly",
HeavenlyStem::Ding => "dingHeavenly",
HeavenlyStem::Wu => "wuHeavenly",
HeavenlyStem::Ji => "jiHeavenly",
HeavenlyStem::Geng => "gengHeavenly",
HeavenlyStem::Xin => "xinHeavenly",
HeavenlyStem::Ren => "renHeavenly",
HeavenlyStem::Gui => "guiHeavenly",
}
}
}
impl EarthlyBranch {
pub fn as_key(&self) -> &'static str {
match self {
EarthlyBranch::Zi => "ziEarthly",
EarthlyBranch::Chou => "chouEarthly",
EarthlyBranch::Yin => "yinEarthly",
EarthlyBranch::Mao => "maoEarthly",
EarthlyBranch::Chen => "chenEarthly",
EarthlyBranch::Si => "siEarthly",
EarthlyBranch::Wu => "wuEarthly",
EarthlyBranch::Wei => "weiEarthly",
EarthlyBranch::Shen => "shenEarthly",
EarthlyBranch::You => "youEarthly",
EarthlyBranch::Xu => "xuEarthly",
EarthlyBranch::Hai => "haiEarthly",
}
}
}
impl Mutagen {
pub fn as_key(&self) -> &'static str {
match self {
Mutagen::Lu => "sihuaLu",
Mutagen::Quan => "sihuaQuan",
Mutagen::Ke => "sihuaKe",
Mutagen::Ji => "sihuaJi",
}
}
}
impl Brightness {
pub fn as_key(&self) -> &'static str {
match self {
Brightness::Miao => "miao",
Brightness::Wang => "wang",
Brightness::De => "de",
Brightness::Li => "li",
Brightness::Ping => "ping",
Brightness::Bu => "bu",
Brightness::Xian => "xian",
}
}
}
impl FiveElementsClass {
pub fn as_key(&self) -> &'static str {
match self {
FiveElementsClass::Water2nd => "water2nd",
FiveElementsClass::Wood3rd => "wood3rd",
FiveElementsClass::Metal4th => "metal4th",
FiveElementsClass::Earth5th => "earth5th",
FiveElementsClass::Fire6th => "fire6th",
}
}
}
impl Palace {
pub fn from_key(key: &str) -> Option<Palace> {
match key {
"soulPalace" => Some(Palace::Soul),
"parentsPalace" => Some(Palace::Parents),
"spiritPalace" => Some(Palace::Spirit),
"propertyPalace" => Some(Palace::Property),
"careerPalace" => Some(Palace::Career),
"friendsPalace" => Some(Palace::Friends),
"surfacePalace" => Some(Palace::Surface),
"healthPalace" => Some(Palace::Health),
"wealthPalace" => Some(Palace::Wealth),
"childrenPalace" => Some(Palace::Children),
"spousePalace" => Some(Palace::Spouse),
"siblingsPalace" => Some(Palace::Siblings),
_ => None,
}
}
}
impl HeavenlyStem {
pub fn from_key(key: &str) -> Option<HeavenlyStem> {
match key {
"jiaHeavenly" => Some(HeavenlyStem::Jia),
"yiHeavenly" => Some(HeavenlyStem::Yi),
"bingHeavenly" => Some(HeavenlyStem::Bing),
"dingHeavenly" => Some(HeavenlyStem::Ding),
"wuHeavenly" => Some(HeavenlyStem::Wu),
"jiHeavenly" => Some(HeavenlyStem::Ji),
"gengHeavenly" => Some(HeavenlyStem::Geng),
"xinHeavenly" => Some(HeavenlyStem::Xin),
"renHeavenly" => Some(HeavenlyStem::Ren),
"guiHeavenly" => Some(HeavenlyStem::Gui),
_ => None,
}
}
}
impl EarthlyBranch {
pub fn from_key(key: &str) -> Option<EarthlyBranch> {
match key {
"ziEarthly" => Some(EarthlyBranch::Zi),
"chouEarthly" => Some(EarthlyBranch::Chou),
"yinEarthly" => Some(EarthlyBranch::Yin),
"maoEarthly" => Some(EarthlyBranch::Mao),
"chenEarthly" => Some(EarthlyBranch::Chen),
"siEarthly" => Some(EarthlyBranch::Si),
"wuEarthly" => Some(EarthlyBranch::Wu),
"weiEarthly" => Some(EarthlyBranch::Wei),
"shenEarthly" => Some(EarthlyBranch::Shen),
"youEarthly" => Some(EarthlyBranch::You),
"xuEarthly" => Some(EarthlyBranch::Xu),
"haiEarthly" => Some(EarthlyBranch::Hai),
_ => None,
}
}
}
impl Mutagen {
pub fn from_key(key: &str) -> Option<Mutagen> {
match key {
"sihuaLu" => Some(Mutagen::Lu),
"sihuaQuan" => Some(Mutagen::Quan),
"sihuaKe" => Some(Mutagen::Ke),
"sihuaJi" => Some(Mutagen::Ji),
_ => None,
}
}
}
impl Brightness {
pub fn from_key(key: &str) -> Option<Brightness> {
match key {
"miao" => Some(Brightness::Miao),
"wang" => Some(Brightness::Wang),
"de" => Some(Brightness::De),
"li" => Some(Brightness::Li),
"ping" => Some(Brightness::Ping),
"bu" => Some(Brightness::Bu),
"xian" => Some(Brightness::Xian),
_ => None,
}
}
}
impl FiveElementsClass {
pub fn from_key(key: &str) -> Option<FiveElementsClass> {
match key {
"water2nd" => Some(FiveElementsClass::Water2nd),
"wood3rd" => Some(FiveElementsClass::Wood3rd),
"metal4th" => Some(FiveElementsClass::Metal4th),
"earth5th" => Some(FiveElementsClass::Earth5th),
"fire6th" => Some(FiveElementsClass::Fire6th),
_ => None,
}
}
}
#[cfg(test)]
mod key_roundtrip_tests {
use super::*;
use crate::astro::builder::by_solar;
use crate::data::constants::{EARTHLY_BRANCHES, HEAVENLY_STEMS, PALACES};
use crate::data::stars::StarKey;
#[test]
fn test_language_code_aliases() {
for lang in [
Language::ZhCN,
Language::ZhTW,
Language::EnUS,
Language::JaJP,
Language::KoKR,
Language::ViVN,
] {
let code = lang.as_code();
assert_eq!(Language::from_code(code), Some(lang));
assert_eq!(Language::from_code(&code.to_lowercase()), Some(lang));
assert_eq!(Language::from_code(&code.replace('-', "_")), Some(lang));
}
assert_eq!(Language::from_code("zh_cn"), Some(Language::ZhCN));
assert_eq!(Language::from_code("klingon"), None);
}
#[test]
fn test_enum_key_roundtrip() {
for p in PALACES {
assert_eq!(Palace::from_key(p.as_key()), Some(p), "{p:?}");
}
for s in HEAVENLY_STEMS {
assert_eq!(HeavenlyStem::from_key(s.as_key()), Some(s), "{s:?}");
}
for b in EARTHLY_BRANCHES {
assert_eq!(EarthlyBranch::from_key(b.as_key()), Some(b), "{b:?}");
}
for m in [Mutagen::Lu, Mutagen::Quan, Mutagen::Ke, Mutagen::Ji] {
assert_eq!(Mutagen::from_key(m.as_key()), Some(m), "{m:?}");
}
for b in [
Brightness::Miao,
Brightness::Wang,
Brightness::De,
Brightness::Li,
Brightness::Ping,
Brightness::Bu,
Brightness::Xian,
] {
assert_eq!(Brightness::from_key(b.as_key()), Some(b), "{b:?}");
}
for c in [
FiveElementsClass::Water2nd,
FiveElementsClass::Wood3rd,
FiveElementsClass::Metal4th,
FiveElementsClass::Earth5th,
FiveElementsClass::Fire6th,
] {
assert_eq!(FiveElementsClass::from_key(c.as_key()), Some(c), "{c:?}");
}
for v in [YearDivide::Normal, YearDivide::Exact] {
assert_eq!(YearDivide::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [HoroscopeDivide::Normal, HoroscopeDivide::Exact] {
assert_eq!(HoroscopeDivide::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [AgeDivide::Normal, AgeDivide::Birthday] {
assert_eq!(AgeDivide::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [DayDivide::Forward, DayDivide::Current] {
assert_eq!(DayDivide::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [Algorithm::Default, Algorithm::Zhongzhou] {
assert_eq!(Algorithm::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [AstroType::Heaven, AstroType::Earth, AstroType::Human] {
assert_eq!(AstroType::from_key(v.as_key()), Some(v), "{v:?}");
}
for v in [LeapMonth::NotLeap, LeapMonth::Leap, LeapMonth::LeapFixed] {
assert_eq!(LeapMonth::from_key(v.as_key()), Some(v), "{v:?}");
assert_eq!(
LeapMonth::from_flags(v.is_leap_month(), v.fix_leap()),
v,
"{v:?}"
);
}
assert_eq!(LeapMonth::from_flags(false, true), LeapMonth::NotLeap);
assert_eq!(Palace::from_key("bodyPalace"), None);
assert_eq!(StarKey::from_key("nope"), None);
assert_eq!(Algorithm::from_key("normal"), None);
}
#[test]
fn test_star_key_roundtrip_over_chart() {
let chart = by_solar(
"2000-8-16",
2,
Gender::Female,
true,
Language::ZhCN,
Config::default(),
)
.unwrap();
let mut checked = 0;
for palace in &chart.palaces {
for star in palace
.major_stars
.iter()
.chain(palace.minor_stars.iter())
.chain(palace.adjective_stars.iter())
{
assert_eq!(
StarKey::from_key(star.key.as_key()),
Some(star.key),
"{:?} 的 key 往返失败",
star.key
);
checked += 1;
}
assert_eq!(
StarKey::from_key(palace.changsheng12.as_key()),
Some(palace.changsheng12)
);
assert_eq!(
StarKey::from_key(palace.suiqian12.as_key()),
Some(palace.suiqian12)
);
}
assert!(checked > 60, "覆盖的星耀太少:{checked}");
}
}