SylarLong/iztro

View on GitHub
src/astro/FunctionalAstrolabe.ts

Summary

Maintainability
A
0 mins
Test Coverage
import dayjs from 'dayjs';
import { getHeavenlyStemAndEarthlyBranchBySolarDate, normalizeDateStr, solar2lunar } from 'lunar-lite';
import { EARTHLY_BRANCHES } from '../data';
import { Astrolabe, Horoscope, Plugin } from '../data/types';
import { EarthlyBranchKey, EarthlyBranchName, HeavenlyStemName, kot, PalaceName, StarKey, StarName, t } from '../i18n';
import { getHoroscopeStar, getYearly12 } from '../star';
import { IFunctionalStar } from '../star/FunctionalStar';
import { fixEarthlyBranchIndex, fixIndex, getMutagensByHeavenlyStem, timeToIndex } from '../utils';
import { getPalace, getSurroundedPalaces } from './analyzer';
import { IFunctionalPalace } from './FunctionalPalace';
import { IFunctionalSurpalaces } from './FunctionalSurpalaces';
import { getPalaceNames } from './palace';
import FunctionalHoroscope, { IFunctionalHoroscope } from './FunctionalHoroscope';
import { getConfig } from './astro';

/**
 * 获取运限数据
 *
 * @version v0.2.1
 *
 * @private 私有方法
 *
 * @param $ 星盘对象
 * @param targetDate 阳历日期【可选】,默认为调用时日期
 * @param timeIndex 时辰序号【可选】,若不传会返回 `targetDate` 中时间所在的时辰
 * @returns 运限数据
 */
const _getHoroscopeBySolarDate = (
  $: FunctionalAstrolabe,
  targetDate: string | Date = new Date(),
  timeIndex?: number,
): IFunctionalHoroscope => {
  const _birthday = solar2lunar($.solarDate);
  const _date = solar2lunar(targetDate);
  const convertTimeIndex = timeToIndex(dayjs(targetDate).hour());
  const { yearly, monthly, daily, hourly } = getHeavenlyStemAndEarthlyBranchBySolarDate(
    targetDate,
    timeIndex || convertTimeIndex,
    {
      // 运限是以立春为界,但为了满足部分流派允许配置
      year: getConfig().horoscopeDivide,
    },
  );
  // 虚岁
  let nominalAge = _date.lunarYear - _birthday.lunarYear;
  // 是否童限
  let isChildhood = false;

  // 假如目标日期已经过了生日,则需要加1岁
  // 比如 2022年九月初一 出生的人,在出生后虚岁为 1 岁
  // 但在 2023年九月初二 以后,虚岁则为 2 岁
  if (
    (_date.lunarYear === _birthday.lunarYear &&
      _date.lunarMonth === _birthday.lunarMonth &&
      _date.lunarDay > _birthday.lunarDay) ||
    _date.lunarMonth > _birthday.lunarMonth
  ) {
    nominalAge += 1;
  }

  // 大限索引
  let decadalIndex = -1;
  // 大限天干
  let heavenlyStemOfDecade: HeavenlyStemName = 'jia';
  // 大限地支
  let earthlyBranchOfDecade: EarthlyBranchName = 'zi';
  // 小限索引
  let ageIndex = -1;
  // 流年索引
  const yearlyIndex = fixEarthlyBranchIndex(yearly[1]);
  // 流月索引
  let monthlyIndex = -1;
  // 流日索引
  let dailyIndex = -1;
  // 流时索引
  let hourlyIndex = -1;
  // 小限天干
  let heavenlyStemOfAge: HeavenlyStemName = 'jia';
  // 小限地支
  let earthlyBranchOfAge: EarthlyBranchName = 'zi';

  // 查询大限索引
  $.palaces.some(({ decadal }, index) => {
    if (nominalAge >= decadal.range[0] && nominalAge <= decadal.range[1]) {
      decadalIndex = index;
      heavenlyStemOfDecade = decadal.heavenlyStem;
      earthlyBranchOfDecade = decadal.earthlyBranch;

      return true;
    }
  });

  if (decadalIndex < 0) {
    // 如果大限索引小于0则证明还没有开始起运
    // 此时应该取小限运
    // 一命二财三疾厄    四岁夫妻五福德
    // 六岁事业为童限    专就宫垣视吉凶
    const palaces: PalaceName[] = ['命宫', '财帛', '疾厄', '夫妻', '福德', '官禄'];
    const targetIndex = palaces[nominalAge - 1];
    const targetPalace = $.palace(targetIndex);

    if (targetPalace) {
      isChildhood = true;
      decadalIndex = targetPalace.index;
      heavenlyStemOfDecade = targetPalace.heavenlyStem;
      earthlyBranchOfDecade = targetPalace.earthlyBranch;
    }
  }

  // 查询小限索引
  $.palaces.some(({ ages, heavenlyStem, earthlyBranch }, index) => {
    if (ages.includes(nominalAge)) {
      ageIndex = index;
      heavenlyStemOfAge = heavenlyStem;
      earthlyBranchOfAge = earthlyBranch;

      return true;
    }
  });

  // 获取流月索引, 流年地支逆数到生月所在宫位,再从该宫位顺数到生时,为正月所在宫位,之后每月一宫
  monthlyIndex = fixIndex(
    yearlyIndex -
      fixEarthlyBranchIndex($.rawDates.chineseDate.monthly[1]) +
      EARTHLY_BRANCHES.indexOf(kot<EarthlyBranchKey>($.rawDates.chineseDate.hourly[1])) +
      fixEarthlyBranchIndex(monthly[1]),
  );

  // 获取流日索引
  dailyIndex = fixIndex(monthlyIndex + _date.lunarDay - 1);

  // 获取流时索引
  hourlyIndex = fixIndex(dailyIndex + EARTHLY_BRANCHES.indexOf(kot<EarthlyBranchKey>(hourly[1], 'Earthly')));

  const scope: Horoscope = {
    solarDate: normalizeDateStr(targetDate).slice(0, 3).join('-'),
    lunarDate: _date.toString(true),
    decadal: {
      index: decadalIndex,
      name: isChildhood ? t('childhood') : t('decadal'),
      heavenlyStem: t(kot(heavenlyStemOfDecade, 'Heavnly')),
      earthlyBranch: t(kot(earthlyBranchOfDecade, 'Earthly')),
      palaceNames: getPalaceNames(decadalIndex),
      mutagen: getMutagensByHeavenlyStem(heavenlyStemOfDecade),
      stars: getHoroscopeStar(heavenlyStemOfDecade, earthlyBranchOfDecade, 'decadal'),
    },
    age: {
      index: ageIndex,
      nominalAge,
      name: t('turn'),
      heavenlyStem: heavenlyStemOfAge,
      earthlyBranch: earthlyBranchOfAge,
      palaceNames: getPalaceNames(ageIndex),
      mutagen: getMutagensByHeavenlyStem(heavenlyStemOfAge),
    },
    yearly: {
      index: yearlyIndex,
      name: t('yearly'),
      heavenlyStem: t(kot(yearly[0], 'Heavenly')),
      earthlyBranch: t(kot(yearly[1], 'Earthly')),
      palaceNames: getPalaceNames(yearlyIndex),
      mutagen: getMutagensByHeavenlyStem(yearly[0]),
      stars: getHoroscopeStar(yearly[0], yearly[1], 'yearly'),
      yearlyDecStar: getYearly12(targetDate),
    },
    monthly: {
      index: monthlyIndex,
      name: t('monthly'),
      heavenlyStem: t(kot(monthly[0], 'Heavenly')),
      earthlyBranch: t(kot(monthly[1], 'Earthly')),
      palaceNames: getPalaceNames(monthlyIndex),
      mutagen: getMutagensByHeavenlyStem(monthly[0]),
      stars: getHoroscopeStar(monthly[0], monthly[1], 'monthly'),
    },
    daily: {
      index: dailyIndex,
      name: t('daily'),
      heavenlyStem: t(kot(daily[0], 'Heavenly')),
      earthlyBranch: t(kot(daily[1], 'Earthly')),
      palaceNames: getPalaceNames(dailyIndex),
      mutagen: getMutagensByHeavenlyStem(daily[0]),
      stars: getHoroscopeStar(daily[0], daily[1], 'daily'),
    },
    hourly: {
      index: hourlyIndex,
      name: t('hourly'),
      heavenlyStem: t(kot(hourly[0], 'Heavenly')),
      earthlyBranch: t(kot(hourly[1], 'Earthly')),
      palaceNames: getPalaceNames(hourlyIndex),
      mutagen: getMutagensByHeavenlyStem(hourly[0]),
      stars: getHoroscopeStar(hourly[0], hourly[1], 'hourly'),
    },
  };

  return new FunctionalHoroscope(scope, $);
};

/**
 * 星盘类接口定义。
 *
 * 文档地址:https://docs.iztro.com/posts/astrolabe.html#functionalastrolabe
 */
export interface IFunctionalAstrolabe extends Astrolabe {
  /**
   * 插件注入方法
   *
   * @version v2.3.0
   *
   * @param plugin 插件函数
   */
  use(plugin: Plugin): void;
  /**
   * 获取运限数据
   *
   * @version v0.2.0
   *
   * @param date 阳历日期【可选】,默认为调用时的日期
   * @param timeIndex 时辰索引【可选】,默认会自动读取当前时间的时辰
   * @returns 运限数据
   */
  horoscope: (date?: string | Date, timeIndex?: number) => IFunctionalHoroscope;

  /**
   * 通过星耀名称获取到当前星耀的对象实例
   *
   * @version v1.2.0
   *
   * @param starName 星耀名称
   * @returns 星耀实例
   */
  star: (starName: StarName) => IFunctionalStar;

  /**
   * 获取星盘的某一个宫位
   *
   * @version v1.0.0
   *
   * @param indexOrName 宫位索引或者宫位名称
   * @returns 对应的宫位数据,若没有找到则返回undefined
   */
  palace: (indexOrName: number | PalaceName) => IFunctionalPalace | undefined;

  /**
   * 获取三方四正宫位,所谓三方四正就是传入的目标宫位,以及其对宫,财帛位和官禄位,总共四个宫位
   *
   * @version v1.1.0
   *
   * @param indexOrName 宫位索引或者宫位名称
   * @returns 三方四正宫位
   */
  surroundedPalaces: (indexOrName: number | PalaceName) => IFunctionalSurpalaces;

  /**
   *
   * 判断某一个宫位三方四正是否包含目标星耀,必须要全部包含才会返回true
   *
   * @version v1.0.0
   *
   * @param indexOrName 宫位索引或者宫位名称
   * @param stars 星耀名称数组
   * @returns true | false
   */
  isSurrounded: (indexOrName: number | PalaceName, stars: StarName[]) => boolean;

  /**
   * 判断三方四正内是否有传入星耀的其中一个,只要命中一个就会返回true
   *
   * @version v1.1.0
   * @deprecated v1.2.0
   *
   * @param indexOrName 宫位索引或者宫位名称
   * @param stars 星耀名称数组
   * @returns true | false
   */
  isSurroundedOneOf: (indexOrName: number | PalaceName, stars: StarName[]) => boolean;

  /**
   * 判断某一个宫位三方四正是否不含目标星耀,必须要全部都不在三方四正内含才会返回true
   *
   * @version v1.1.0
   * @deprecated v1.2.0
   *
   * @param indexOrName 宫位索引或者宫位名称
   * @param stars 星耀名称数组
   * @returns true | false
   */
  notSurrounded: (indexOrName: number | PalaceName, stars: StarName[]) => boolean;
}

/**
 * 星盘类。
 *
 * 文档地址:https://docs.iztro.com/posts/astrolabe.html#functionalastrolabe
 */
export default class FunctionalAstrolabe implements IFunctionalAstrolabe {
  gender;
  solarDate;
  lunarDate;
  chineseDate;
  rawDates;
  time;
  timeRange;
  sign;
  zodiac;
  earthlyBranchOfSoulPalace;
  earthlyBranchOfBodyPalace;
  soul;
  body;
  fiveElementsClass;
  palaces;
  copyright;

  // 保存插件列表
  private plugins: Plugin[] = [];

  constructor(data: Astrolabe) {
    this.gender = data.gender;
    this.solarDate = data.solarDate;
    this.lunarDate = data.lunarDate;
    this.chineseDate = data.chineseDate;
    this.rawDates = data.rawDates;
    this.time = data.time;
    this.timeRange = data.timeRange;
    this.sign = data.sign;
    this.zodiac = data.zodiac;
    this.earthlyBranchOfBodyPalace = data.earthlyBranchOfBodyPalace;
    this.earthlyBranchOfSoulPalace = data.earthlyBranchOfSoulPalace;
    this.soul = data.soul;
    this.body = data.body;
    this.fiveElementsClass = data.fiveElementsClass;
    this.palaces = data.palaces;
    this.copyright = data.copyright;

    return this;
  }

  use(plugin: Plugin): void {
    this.plugins.push(plugin);
    plugin.apply(this);
  }

  star = (starName: StarName): IFunctionalStar => {
    let targetStar: IFunctionalStar | undefined;

    this.palaces.some((p) => {
      [...p.majorStars, ...p.minorStars, ...p.adjectiveStars].some((item) => {
        if (kot<StarKey>(item.name) === kot<StarKey>(starName)) {
          targetStar = item;
          targetStar.setPalace(p);
          targetStar.setAstrolabe(this);
        }
      });
    });

    if (!targetStar) {
      throw new Error('invalid star name.');
    }

    return targetStar;
  };

  horoscope = (targetDate: string | Date = new Date(), timeIndexOfTarget?: number) =>
    _getHoroscopeBySolarDate(this, targetDate, timeIndexOfTarget);

  palace = (indexOrName: number | PalaceName): IFunctionalPalace | undefined => getPalace(this, indexOrName);

  surroundedPalaces = (indexOrName: number | PalaceName): IFunctionalSurpalaces =>
    getSurroundedPalaces(this, indexOrName);

  /**
   * @deprecated 此方法已在`v1.2.0`废弃,请用下列方法替换
   *
   * @example
   *  // AS IS
   *  astrolabe.isSurrounded(0, ["紫微"]);
   *
   *  // TO BE
   *  astrolabe.surroundedPalaces(0).have(["紫微"]);
   */
  isSurrounded = (indexOrName: number | PalaceName, stars: StarName[]): boolean =>
    this.surroundedPalaces(indexOrName).have(stars);

  /**
   * @deprecated 此方法已在`v1.2.0`废弃,请用下列方法替换
   *
   * @example
   *  // AS IS
   *  astrolabe.isSurroundedOneOf(0, ["紫微"]);
   *
   *  // TO BE
   *  astrolabe.surroundedPalaces(0).haveOneOf(["紫微"]);
   */
  isSurroundedOneOf = (indexOrName: number | PalaceName, stars: StarName[]): boolean =>
    this.surroundedPalaces(indexOrName).haveOneOf(stars);

  /**
   * @deprecated 此方法已在`v1.2.0`废弃,请用下列方法替换
   *
   * @example
   *  // AS IS
   *  astrolabe.notSurrounded(0, ["紫微"]);
   *
   *  // TO BE
   *  astrolabe.surroundedPalaces(0).notHave(["紫微"]);
   */
  notSurrounded = (indexOrName: number | PalaceName, stars: StarName[]): boolean =>
    this.surroundedPalaces(indexOrName).notHave(stars);
}