use serde::{Deserialize, Serialize};
use crate::data::types::*;
use crate::i18n::{
translate_brightness, translate_earthly_branch, translate_five_elements_class,
translate_gender, translate_heavenly_stem, translate_mutagen, translate_palace, translate_star,
};
use crate::models::astrolabe::Astrolabe;
use crate::models::horoscope::{HoroscopeData, HoroscopeItem};
use crate::models::palace::PalaceData;
use crate::models::star::Star;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StarDto {
pub key: String,
pub name: String,
#[serde(rename = "type")]
pub star_type: String,
pub scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub brightness: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brightness_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutagen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutagen_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DecadalDto {
pub range: [u32; 2],
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PalaceDto {
pub index: usize,
pub name: String,
pub name_key: String,
pub is_body_palace: bool,
pub is_original_palace: bool,
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
pub major_stars: Vec<StarDto>,
pub minor_stars: Vec<StarDto>,
pub adjective_stars: Vec<StarDto>,
pub changsheng12: String,
pub changsheng12_key: String,
pub boshi12: String,
pub boshi12_key: String,
pub jiangqian12: String,
pub jiangqian12_key: String,
pub suiqian12: String,
pub suiqian12_key: String,
pub decadal: DecadalDto,
pub ages: Vec<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawLunarDateDto {
pub lunar_year: i64,
pub lunar_month: u32,
pub lunar_day: u32,
pub is_leap: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawChineseDateDto {
pub yearly: [String; 2],
pub monthly: [String; 2],
pub daily: [String; 2],
pub hourly: [String; 2],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawDatesDto {
pub lunar_date: RawLunarDateDto,
pub chinese_date: RawChineseDateDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigDto {
pub year_divide: String,
pub horoscope_divide: String,
pub age_divide: String,
pub day_divide: String,
pub algorithm: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AstrolabeDto {
pub gender: String,
pub solar_date: String,
pub lunar_date: String,
pub chinese_date: String,
pub raw_dates: RawDatesDto,
pub time: String,
pub time_range: String,
pub sign: String,
pub zodiac: String,
pub earthly_branch_of_soul_palace: String,
pub earthly_branch_of_soul_palace_key: String,
pub earthly_branch_of_body_palace: String,
pub earthly_branch_of_body_palace_key: String,
pub soul: String,
pub soul_key: String,
pub body: String,
pub body_key: String,
pub five_elements_class: String,
pub five_elements_class_key: String,
pub palaces: Vec<PalaceDto>,
pub gender_key: String,
pub time_index: u8,
pub fix_leap: bool,
pub language: String,
pub config: ConfigDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoroscopeScopeDto {
pub index: usize,
pub name: String,
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
pub palace_names: Vec<String>,
pub palace_name_keys: Vec<String>,
pub mutagen: Vec<String>,
pub mutagen_keys: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stars: Option<Vec<Vec<StarDto>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgeDto {
#[serde(flatten)]
pub base: HoroscopeScopeDto,
pub nominal_age: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YearlyDecStarDto {
pub suiqian12: Vec<String>,
#[serde(rename = "suiqian12Keys")]
pub suiqian12_keys: Vec<String>,
pub jiangqian12: Vec<String>,
#[serde(rename = "jiangqian12Keys")]
pub jiangqian12_keys: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct YearlyDto {
#[serde(flatten)]
pub base: HoroscopeScopeDto,
pub yearly_dec_star: YearlyDecStarDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoroscopeDto {
pub lunar_date: String,
pub solar_date: String,
pub decadal: HoroscopeScopeDto,
pub age: AgeDto,
pub yearly: YearlyDto,
pub monthly: HoroscopeScopeDto,
pub daily: HoroscopeScopeDto,
pub hourly: HoroscopeScopeDto,
}
fn star_type_str(t: StarType) -> &'static str {
match t {
StarType::Major => "major",
StarType::Soft => "soft",
StarType::Tough => "tough",
StarType::Adjective => "adjective",
StarType::Flower => "flower",
StarType::Helper => "helper",
StarType::Lucun => "lucun",
StarType::Tianma => "tianma",
}
}
fn scope_str(s: Scope) -> &'static str {
match s {
Scope::Origin => "origin",
Scope::Decadal => "decadal",
Scope::Yearly => "yearly",
Scope::Monthly => "monthly",
Scope::Daily => "daily",
Scope::Hourly => "hourly",
}
}
pub fn language_str(l: Language) -> &'static str {
match l {
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(crate) fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.unwrap_or("computation panicked")
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
struct ConfigPatch {
year_divide: Option<String>,
horoscope_divide: Option<String>,
age_divide: Option<String>,
day_divide: Option<String>,
algorithm: Option<String>,
}
pub fn parse_config_json(json: Option<&str>) -> Result<Config, String> {
let json = match json {
None => return Ok(Config::default()),
Some(s) if s.trim().is_empty() => return Ok(Config::default()),
Some(s) => s,
};
let patch: ConfigPatch =
serde_json::from_str(json).map_err(|e| format!("Invalid config JSON: {e}"))?;
let mut config = Config::default();
if let Some(v) = patch.year_divide {
config.year_divide = match v.as_str() {
"normal" => YearDivide::Normal,
"exact" => YearDivide::Exact,
_ => {
return Err(format!(
"Invalid yearDivide '{v}'. Expected 'normal' or 'exact'."
));
}
};
}
if let Some(v) = patch.horoscope_divide {
config.horoscope_divide = match v.as_str() {
"normal" => HoroscopeDivide::Normal,
"exact" => HoroscopeDivide::Exact,
_ => {
return Err(format!(
"Invalid horoscopeDivide '{v}'. Expected 'normal' or 'exact'."
));
}
};
}
if let Some(v) = patch.age_divide {
config.age_divide = match v.as_str() {
"normal" => AgeDivide::Normal,
"birthday" => AgeDivide::Birthday,
_ => {
return Err(format!(
"Invalid ageDivide '{v}'. Expected 'normal' or 'birthday'."
));
}
};
}
if let Some(v) = patch.day_divide {
config.day_divide = match v.as_str() {
"forward" => DayDivide::Forward,
"current" => DayDivide::Current,
_ => {
return Err(format!(
"Invalid dayDivide '{v}'. Expected 'forward' or 'current'."
));
}
};
}
if let Some(v) = patch.algorithm {
config.algorithm = match v.as_str() {
"default" => Algorithm::Default,
"zhongzhou" => Algorithm::Zhongzhou,
_ => {
return Err(format!(
"Invalid algorithm '{v}'. Expected 'default' or 'zhongzhou'."
));
}
};
}
Ok(config)
}
impl From<Config> for ConfigDto {
fn from(c: Config) -> Self {
ConfigDto {
year_divide: match c.year_divide {
YearDivide::Normal => "normal",
YearDivide::Exact => "exact",
}
.to_string(),
horoscope_divide: match c.horoscope_divide {
HoroscopeDivide::Normal => "normal",
HoroscopeDivide::Exact => "exact",
}
.to_string(),
age_divide: match c.age_divide {
AgeDivide::Normal => "normal",
AgeDivide::Birthday => "birthday",
}
.to_string(),
day_divide: match c.day_divide {
DayDivide::Forward => "forward",
DayDivide::Current => "current",
}
.to_string(),
algorithm: match c.algorithm {
Algorithm::Default => "default",
Algorithm::Zhongzhou => "zhongzhou",
}
.to_string(),
}
}
}
fn is_mutagen_candidate(key: crate::data::stars::StarKey) -> bool {
use crate::data::stars::StarKey::*;
matches!(
key,
ZiweiMaj
| TianjiMaj
| TaiyangMaj
| WuquMaj
| TiantongMaj
| LianzhenMaj
| TianfuMaj
| TaiyinMaj
| TanlangMaj
| JumenMaj
| TianxiangMaj
| TianliangMaj
| QishaMaj
| PojunMaj
| WenchangMin
| WenquMin
| ZuofuMin
| YoubiMin
)
}
fn primary_star_dto(s: &Star, lang: Language) -> StarDto {
StarDto {
key: s.key.as_key().to_string(),
name: s.name.clone(),
star_type: star_type_str(s.star_type).to_string(),
scope: scope_str(s.scope).to_string(),
brightness: Some(
s.brightness
.map(|b| translate_brightness(b, lang).to_string())
.unwrap_or_default(),
),
brightness_key: s.brightness.map(|b| b.as_key().to_string()),
mutagen: if is_mutagen_candidate(s.key) {
Some(
s.mutagen
.map(|m| translate_mutagen(m, lang).to_string())
.unwrap_or_default(),
)
} else {
None
},
mutagen_key: s.mutagen.map(|m| m.as_key().to_string()),
}
}
fn bare_star_dto(s: &Star) -> StarDto {
StarDto {
key: s.key.as_key().to_string(),
name: s.name.clone(),
star_type: star_type_str(s.star_type).to_string(),
scope: scope_str(s.scope).to_string(),
brightness: None,
brightness_key: None,
mutagen: None,
mutagen_key: None,
}
}
fn pillar(p: (HeavenlyStem, EarthlyBranch), lang: Language) -> [String; 2] {
[
translate_heavenly_stem(p.0, lang).to_string(),
translate_earthly_branch(p.1, lang).to_string(),
]
}
fn palace_dto(p: &PalaceData, lang: Language) -> PalaceDto {
PalaceDto {
index: p.index,
name: translate_palace(p.name, lang).to_string(),
name_key: p.name.as_key().to_string(),
is_body_palace: p.is_body_palace,
is_original_palace: p.is_original_palace,
heavenly_stem: translate_heavenly_stem(p.heavenly_stem, lang).to_string(),
heavenly_stem_key: p.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(p.earthly_branch, lang).to_string(),
earthly_branch_key: p.earthly_branch.as_key().to_string(),
major_stars: p
.major_stars
.iter()
.map(|s| primary_star_dto(s, lang))
.collect(),
minor_stars: p
.minor_stars
.iter()
.map(|s| primary_star_dto(s, lang))
.collect(),
adjective_stars: p.adjective_stars.iter().map(bare_star_dto).collect(),
changsheng12: translate_star(p.changsheng12, lang).to_string(),
changsheng12_key: p.changsheng12.as_key().to_string(),
boshi12: translate_star(p.boshi12, lang).to_string(),
boshi12_key: p.boshi12.as_key().to_string(),
jiangqian12: translate_star(p.jiangqian12, lang).to_string(),
jiangqian12_key: p.jiangqian12.as_key().to_string(),
suiqian12: translate_star(p.suiqian12, lang).to_string(),
suiqian12_key: p.suiqian12.as_key().to_string(),
decadal: DecadalDto {
range: [p.decadal.range.0, p.decadal.range.1],
heavenly_stem: translate_heavenly_stem(p.decadal.heavenly_stem, lang).to_string(),
heavenly_stem_key: p.decadal.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(p.decadal.earthly_branch, lang).to_string(),
earthly_branch_key: p.decadal.earthly_branch.as_key().to_string(),
},
ages: p.ages.clone(),
}
}
impl Astrolabe {
pub fn to_dto(&self) -> AstrolabeDto {
let lang = self.language;
AstrolabeDto {
gender: translate_gender(self.gender, lang).to_string(),
solar_date: self.solar_date.clone(),
lunar_date: self.lunar_date.clone(),
chinese_date: self.chinese_date.clone(),
raw_dates: RawDatesDto {
lunar_date: RawLunarDateDto {
lunar_year: self.raw_dates.lunar_date.lunar_year,
lunar_month: self.raw_dates.lunar_date.lunar_month,
lunar_day: self.raw_dates.lunar_date.lunar_day,
is_leap: self.raw_dates.lunar_date.is_leap,
},
chinese_date: RawChineseDateDto {
yearly: pillar(self.raw_dates.chinese_date.yearly, Language::ZhCN),
monthly: pillar(self.raw_dates.chinese_date.monthly, Language::ZhCN),
daily: pillar(self.raw_dates.chinese_date.daily, Language::ZhCN),
hourly: pillar(self.raw_dates.chinese_date.hourly, Language::ZhCN),
},
},
time: self.time.clone(),
time_range: self.time_range.clone(),
sign: self.sign.clone(),
zodiac: self.zodiac.clone(),
earthly_branch_of_soul_palace: translate_earthly_branch(
self.earthly_branch_of_soul_palace,
lang,
)
.to_string(),
earthly_branch_of_soul_palace_key: self
.earthly_branch_of_soul_palace
.as_key()
.to_string(),
earthly_branch_of_body_palace: translate_earthly_branch(
self.earthly_branch_of_body_palace,
lang,
)
.to_string(),
earthly_branch_of_body_palace_key: self
.earthly_branch_of_body_palace
.as_key()
.to_string(),
soul: translate_star(self.soul, lang).to_string(),
soul_key: self.soul.as_key().to_string(),
body: translate_star(self.body, lang).to_string(),
body_key: self.body.as_key().to_string(),
five_elements_class: translate_five_elements_class(self.five_elements_class, lang)
.to_string(),
five_elements_class_key: self.five_elements_class.as_key().to_string(),
palaces: self.palaces.iter().map(|p| palace_dto(p, lang)).collect(),
gender_key: match self.gender {
Gender::Male => "male",
Gender::Female => "female",
}
.to_string(),
time_index: self.time_index,
fix_leap: self.fix_leap,
language: language_str(lang).to_string(),
config: self.config.into(),
}
}
}
fn scope_dto(item: &HoroscopeItem, lang: Language) -> HoroscopeScopeDto {
HoroscopeScopeDto {
index: item.index,
name: item.name.clone(),
heavenly_stem: translate_heavenly_stem(item.heavenly_stem, lang).to_string(),
heavenly_stem_key: item.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(item.earthly_branch, lang).to_string(),
earthly_branch_key: item.earthly_branch.as_key().to_string(),
palace_names: item
.palace_names
.iter()
.map(|p| translate_palace(*p, lang).to_string())
.collect(),
palace_name_keys: item
.palace_names
.iter()
.map(|p| p.as_key().to_string())
.collect(),
mutagen: item
.mutagen
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
mutagen_keys: item
.mutagen
.iter()
.map(|k| k.as_key().to_string())
.collect(),
stars: item.stars.as_ref().map(|groups| {
groups
.iter()
.map(|g| g.iter().map(bare_star_dto).collect())
.collect()
}),
}
}
impl HoroscopeData {
pub fn to_dto(&self, lang: Language) -> HoroscopeDto {
HoroscopeDto {
lunar_date: self.lunar_date.clone(),
solar_date: self.solar_date.clone(),
decadal: scope_dto(&self.decadal, lang),
age: AgeDto {
base: scope_dto(&self.age.base, lang),
nominal_age: self.age.nominal_age,
},
yearly: YearlyDto {
base: scope_dto(&self.yearly.base, lang),
yearly_dec_star: YearlyDecStarDto {
suiqian12: self
.yearly
.yearly_dec_star
.suiqian12
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
suiqian12_keys: self
.yearly
.yearly_dec_star
.suiqian12
.iter()
.map(|k| k.as_key().to_string())
.collect(),
jiangqian12: self
.yearly
.yearly_dec_star
.jiangqian12
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
jiangqian12_keys: self
.yearly
.yearly_dec_star
.jiangqian12
.iter()
.map(|k| k.as_key().to_string())
.collect(),
},
},
monthly: scope_dto(&self.monthly, lang),
daily: scope_dto(&self.daily, lang),
hourly: scope_dto(&self.hourly, lang),
}
}
}