diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9b286bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+vendor/
+composer.lock
+composer.phar
+.phpunit.result.cache
+.DS_Store
+package.xml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e0bf586
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+# Tyme [![License](https://img.shields.io/badge/license-MIT-4EB1BA.svg?style=flat-square)](https://github.com/6tail/tyme4php/blob/master/LICENSE)
+
+Tyme是一个非常强大的日历工具库,可以看作 [Lunar](https://6tail.cn/calendar/api.html "https://6tail.cn/calendar/api.html") 的升级版,拥有更优的设计和扩展性,支持公历和农历、星座、干支、生肖、节气、法定假日等。
+
+
+> 基于php8.1开发。
+
+## composer
+
+ composer require 6tail/tyme4php
+
+ getLunarDay();
+
+## 单文件版本
+
+1. 下载本源代码,执行tools/build-standalone.php
,可在tools目录下生成Tyme.php
单文件。
+2. 可在 [Releases](https://github.com/6tail/tyme4php/releases) 中下载对应版本的Tyme.php
单文件。
+
+
+ getLunarDay();
+
+## 文档
+
+请移步至 [https://6tail.cn/tyme.html](https://6tail.cn/tyme.html "https://6tail.cn/tyme.html")
+
+## Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=6tail/tyme4php&type=Date)](https://star-history.com/#6tail/tyme4php&Date)
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..931d927
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,36 @@
+{
+ "name": "6tail/tyme4php",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "6tail",
+ "email": "6tail@6tail.cn",
+ "homepage": "https://6tail.cn"
+ }
+ ],
+ "minimum-stability": "stable",
+ "require": {
+ "php": ">=8.1",
+ "ext-bcmath": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "com\\tyme\\": "src/"
+ }
+ },
+ "homepage": "https://github.com/6tail/tyme4php",
+ "keywords": [
+ "公历",
+ "农历",
+ "星座",
+ "干支",
+ "生肖",
+ "节气",
+ "法定假日"
+ ],
+ "description": "a calendar library"
+}
diff --git a/src/AbstractCulture.php b/src/AbstractCulture.php
new file mode 100644
index 0000000..cc0888f
--- /dev/null
+++ b/src/AbstractCulture.php
@@ -0,0 +1,48 @@
+getName();
+ }
+
+ /**
+ * @param mixed $o 对象
+ * @return bool true/false
+ */
+ function equals(mixed $o): bool
+ {
+ return $o instanceof Culture && $this->__toString() == $o->__toString();
+ }
+
+ /**
+ * 转换为不超范围的索引
+ *
+ * @param int|null $index 索引
+ * @param string|null $name 名称
+ * @param int|null $size 数量
+ * @return int 索引,从0开始
+ */
+ protected function indexOf(int $index = null, string $name = null, int $size = null): int
+ {
+ if ($index !== null && $size !== null) {
+ $i = $index % $size;
+ if ($i < 0) {
+ $i += $size;
+ }
+ return $i;
+ }
+ throw new InvalidArgumentException(sprintf('invalid name: %s, size: %d', $name, $size));
+ }
+}
diff --git a/src/AbstractCultureDay.php b/src/AbstractCultureDay.php
new file mode 100644
index 0000000..507c666
--- /dev/null
+++ b/src/AbstractCultureDay.php
@@ -0,0 +1,53 @@
+culture = $culture;
+ $this->dayIndex = $dayIndex;
+ }
+
+ /**
+ * 天索引
+ *
+ * @return int 索引
+ */
+ function getDayIndex(): int
+ {
+ return $this->dayIndex;
+ }
+
+ protected function getCulture(): Culture
+ {
+ return $this->culture;
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s第%d天', $this->culture, $this->dayIndex + 1);
+ }
+
+ function getName(): string
+ {
+ return $this->culture->getName();
+ }
+}
diff --git a/src/AbstractTyme.php b/src/AbstractTyme.php
new file mode 100644
index 0000000..abc4763
--- /dev/null
+++ b/src/AbstractTyme.php
@@ -0,0 +1,13 @@
+names = $names;
+ if ($index !== null) {
+ $this->index = $this->indexOf($index);
+ } else if ($name !== null) {
+ $this->index = $this->indexOf(null, $name);
+ }
+ }
+
+ /**
+ * 名称
+ *
+ * @return string 名称
+ */
+ function getName(): string
+ {
+ return $this->names[$this->index];
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引,从0开始
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ /**
+ * 数量
+ *
+ * @return int 数量
+ */
+ function getSize(): int
+ {
+ return count($this->names);
+ }
+
+ protected function indexOf(int $index = null, string $name = null, int $size = null): int
+ {
+ if ($index !== null) {
+ if ($size === null) {
+ return parent::indexOf($index, null, $this->getSize());
+ } else {
+ return parent::indexOf($index, null, $size);
+ }
+ } else if ($name !== null) {
+ // 传了name,则忽略size
+ for ($i = 0, $j = $this->getSize(); $i < $j; $i++) {
+ if ($this->names[$i] == $name) {
+ return $i;
+ }
+ }
+ throw new InvalidArgumentException(sprintf('illegal name: %d', $name));
+ }
+ throw new InvalidArgumentException('need index or name');
+ }
+
+ /**
+ * 推移后的索引
+ *
+ * @param int $n 推移步数
+ * @return int 索引,从0开始
+ */
+ protected function nextIndex(int $n): int
+ {
+ return $this->indexOf($this->index + $n);
+ }
+
+}
diff --git a/src/Tyme.php b/src/Tyme.php
new file mode 100644
index 0000000..db19821
--- /dev/null
+++ b/src/Tyme.php
@@ -0,0 +1,19 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Beast.php b/src/culture/Beast.php
new file mode 100644
index 0000000..fa1b5b8
--- /dev/null
+++ b/src/culture/Beast.php
@@ -0,0 +1,50 @@
+nextIndex($n));
+ }
+
+ /**
+ * 宫
+ *
+ * @return Zone 宫
+ */
+ function getZone(): Zone
+ {
+ return Zone::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/Constellation.php b/src/culture/Constellation.php
new file mode 100644
index 0000000..63eaaf6
--- /dev/null
+++ b/src/culture/Constellation.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Direction.php b/src/culture/Direction.php
new file mode 100644
index 0000000..3432151
--- /dev/null
+++ b/src/culture/Direction.php
@@ -0,0 +1,53 @@
+nextIndex($n));
+ }
+
+ /**
+ * 九野
+ *
+ * @return Land 九野
+ */
+ function getLand(): Land
+ {
+ return Land::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/Duty.php b/src/culture/Duty.php
new file mode 100644
index 0000000..44acb83
--- /dev/null
+++ b/src/culture/Duty.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Element.php b/src/culture/Element.php
new file mode 100644
index 0000000..761ef05
--- /dev/null
+++ b/src/culture/Element.php
@@ -0,0 +1,80 @@
+nextIndex($n));
+ }
+
+ /**
+ * 我生者(生)
+ *
+ * @return Element 五行
+ */
+ function getReinforce(): static
+ {
+ return $this->next(1);
+ }
+
+ /**
+ * 我克者(克)
+ *
+ * @return Element 五行
+ */
+ function getRestrain(): static
+ {
+ return $this->next(2);
+ }
+
+ /**
+ * 生我者(泄)
+ *
+ * @return Element 五行
+ */
+ function getReinforced(): static
+ {
+ return $this->next(-1);
+ }
+
+ /**
+ * 克我者(耗)
+ *
+ * @return Element 五行
+ */
+ function getRestrained(): static
+ {
+ return $this->next(-2);
+ }
+}
diff --git a/src/culture/Land.php b/src/culture/Land.php
new file mode 100644
index 0000000..051b055
--- /dev/null
+++ b/src/culture/Land.php
@@ -0,0 +1,50 @@
+nextIndex($n));
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return Direction::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/Luck.php b/src/culture/Luck.php
new file mode 100644
index 0000000..13f5061
--- /dev/null
+++ b/src/culture/Luck.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Phase.php b/src/culture/Phase.php
new file mode 100644
index 0000000..73c8761
--- /dev/null
+++ b/src/culture/Phase.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Sixty.php b/src/culture/Sixty.php
new file mode 100644
index 0000000..2fccea6
--- /dev/null
+++ b/src/culture/Sixty.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Sound.php b/src/culture/Sound.php
new file mode 100644
index 0000000..cb249db
--- /dev/null
+++ b/src/culture/Sound.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Ten.php b/src/culture/Ten.php
new file mode 100644
index 0000000..4b124ea
--- /dev/null
+++ b/src/culture/Ten.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Terrain.php b/src/culture/Terrain.php
new file mode 100644
index 0000000..678662d
--- /dev/null
+++ b/src/culture/Terrain.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/Twenty.php b/src/culture/Twenty.php
new file mode 100644
index 0000000..c8c5e49
--- /dev/null
+++ b/src/culture/Twenty.php
@@ -0,0 +1,49 @@
+nextIndex($n));
+ }
+
+ /**
+ * 元
+ * @return Sixty 元
+ */
+ function getSixty(): Sixty
+ {
+ return Sixty::fromIndex(intdiv($this->index, 3));
+ }
+}
diff --git a/src/culture/Week.php b/src/culture/Week.php
new file mode 100644
index 0000000..a526d5f
--- /dev/null
+++ b/src/culture/Week.php
@@ -0,0 +1,52 @@
+nextIndex($n));
+ }
+
+
+ /**
+ * 七曜
+ *
+ * @return SevenStar 七曜
+ */
+ function getSevenStar(): SevenStar
+ {
+ return SevenStar::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/Zodiac.php b/src/culture/Zodiac.php
new file mode 100644
index 0000000..d5606c1
--- /dev/null
+++ b/src/culture/Zodiac.php
@@ -0,0 +1,51 @@
+nextIndex($n));
+ }
+
+ /**
+ * 地支
+ *
+ * @return EarthBranch 地支
+ */
+ function getEarthBranch(): EarthBranch
+ {
+ return EarthBranch::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/Zone.php b/src/culture/Zone.php
new file mode 100644
index 0000000..3fdbcd8
--- /dev/null
+++ b/src/culture/Zone.php
@@ -0,0 +1,60 @@
+nextIndex($n));
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return Direction::fromName($this->getName());
+ }
+
+ /**
+ * 神兽
+ *
+ * @return Beast 神兽
+ */
+ function getBeast(): Beast
+ {
+ return Beast::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/dog/Dog.php b/src/culture/dog/Dog.php
new file mode 100644
index 0000000..021fd97
--- /dev/null
+++ b/src/culture/dog/Dog.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/dog/DogDay.php b/src/culture/dog/DogDay.php
new file mode 100644
index 0000000..da55a9b
--- /dev/null
+++ b/src/culture/dog/DogDay.php
@@ -0,0 +1,29 @@
+culture;
+ }
+}
diff --git a/src/culture/fetus/FetusDay.php b/src/culture/fetus/FetusDay.php
new file mode 100644
index 0000000..aaf39c7
--- /dev/null
+++ b/src/culture/fetus/FetusDay.php
@@ -0,0 +1,120 @@
+getSixtyCycle();
+ $this->fetusHeavenStem = new FetusHeavenStem($sixtyCycle->getHeavenStem()->getIndex() % 5);
+ $this->fetusEarthBranch = new FetusEarthBranch($sixtyCycle->getEarthBranch()->getIndex() % 6);
+ $index = [3, 3, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, -9, -9, -9, -9, -9, -5, -5, -1, -1, -1, -3, -7, -7, -7, -7, -5, 7, 7, 7, 7, 7, 7, 2, 2, 2, 2, 2, 3, 3, 3, 3][$sixtyCycle->getIndex()];
+ $this->side = Side::fromCode($index < 0 ? 0 : 1);
+ $this->direction = Direction::fromIndex($index);
+ }
+
+ static function fromLunarDay(LunarDay $lunarDay): static
+ {
+ return new static($lunarDay);
+ }
+
+ function getName(): string
+ {
+ $s = $this->fetusHeavenStem->getName() . $this->fetusEarthBranch->getName();
+ if ('门门' == $s) {
+ $s = '占大门';
+ } else if ('碓磨碓' == $s) {
+ $s = '占碓磨';
+ } else if ('房床床' == $s) {
+ $s = '占房床';
+ } else if (str_starts_with($s, '门')) {
+ $s = '占' . $s;
+ }
+
+ $s .= ' ';
+
+ $directionName = $this->direction->getName();
+ if (Side::IN == $this->side) {
+ $s .= '房';
+ }
+ $s .= $this->side->getName();
+
+ if (Side::OUT == $this->side && str_contains('北南西东', $directionName)) {
+ $s .= '正';
+ }
+ $s .= $directionName;
+ return $s;
+ }
+
+ /**
+ * 内外
+ *
+ * @return Side 侧
+ */
+ function getSide(): Side
+ {
+ return $this->side;
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return $this->direction;
+ }
+
+ /**
+ * 天干六甲胎神
+ *
+ * @return FetusHeavenStem 天干六甲胎神
+ */
+ function getFetusHeavenStem(): FetusHeavenStem
+ {
+ return $this->fetusHeavenStem;
+ }
+
+ /**
+ * 地支六甲胎神
+ *
+ * @return FetusEarthBranch 地支六甲胎神
+ */
+ function getFetusEarthBranch(): FetusEarthBranch
+ {
+ return $this->fetusEarthBranch;
+ }
+}
diff --git a/src/culture/fetus/FetusEarthBranch.php b/src/culture/fetus/FetusEarthBranch.php
new file mode 100644
index 0000000..4a264ca
--- /dev/null
+++ b/src/culture/fetus/FetusEarthBranch.php
@@ -0,0 +1,26 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/fetus/FetusHeavenStem.php b/src/culture/fetus/FetusHeavenStem.php
new file mode 100644
index 0000000..de828a1
--- /dev/null
+++ b/src/culture/fetus/FetusHeavenStem.php
@@ -0,0 +1,26 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/fetus/FetusMonth.php b/src/culture/fetus/FetusMonth.php
new file mode 100644
index 0000000..17cd0a0
--- /dev/null
+++ b/src/culture/fetus/FetusMonth.php
@@ -0,0 +1,43 @@
+isLeap() ? null : new static($lunarMonth->getMonth() - 1);
+ }
+
+ function next(int $n): static
+ {
+ return self::fromIndex($this->nextIndex($n));
+ }
+}
diff --git a/src/culture/nine/Nine.php b/src/culture/nine/Nine.php
new file mode 100644
index 0000000..c1cc286
--- /dev/null
+++ b/src/culture/nine/Nine.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/nine/NineDay.php b/src/culture/nine/NineDay.php
new file mode 100644
index 0000000..a7387b8
--- /dev/null
+++ b/src/culture/nine/NineDay.php
@@ -0,0 +1,29 @@
+culture;
+ }
+}
diff --git a/src/culture/pengzu/PengZu.php b/src/culture/pengzu/PengZu.php
new file mode 100644
index 0000000..5913b55
--- /dev/null
+++ b/src/culture/pengzu/PengZu.php
@@ -0,0 +1,67 @@
+pengZuHeavenStem = PengZuHeavenStem::fromIndex($sixtyCycle->getHeavenStem()->getIndex());
+ $this->pengZuEarthBranch = PengZuEarthBranch::fromIndex($sixtyCycle->getEarthBranch()->getIndex());
+ }
+
+ /**
+ * 从干支初始化
+ *
+ * @param SixtyCycle $sixtyCycle 干支
+ * @return PengZu 彭祖百忌
+ */
+ static function fromSixtyCycle(SixtyCycle $sixtyCycle): static
+ {
+ return new static($sixtyCycle);
+ }
+
+ function getName(): string
+ {
+ return sprintf('%s %s', $this->pengZuHeavenStem, $this->pengZuEarthBranch);
+ }
+
+ /**
+ * 天干彭祖百忌
+ *
+ * @return PengZuHeavenStem 天干彭祖百忌
+ */
+ function getPengZuHeavenStem(): PengZuHeavenStem
+ {
+ return $this->pengZuHeavenStem;
+ }
+
+ /**
+ * 地支彭祖百忌
+ *
+ * @return PengZuEarthBranch 地支彭祖百忌
+ */
+ function getPengZuEarthBranch(): PengZuEarthBranch
+ {
+ return $this->pengZuEarthBranch;
+ }
+}
diff --git a/src/culture/pengzu/PengZuEarthBranch.php b/src/culture/pengzu/PengZuEarthBranch.php
new file mode 100644
index 0000000..4b201e2
--- /dev/null
+++ b/src/culture/pengzu/PengZuEarthBranch.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/pengzu/PengZuHeavenStem.php b/src/culture/pengzu/PengZuHeavenStem.php
new file mode 100644
index 0000000..8e1ce4e
--- /dev/null
+++ b/src/culture/pengzu/PengZuHeavenStem.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/phenology/Phenology.php b/src/culture/phenology/Phenology.php
new file mode 100644
index 0000000..b89866a
--- /dev/null
+++ b/src/culture/phenology/Phenology.php
@@ -0,0 +1,51 @@
+nextIndex($n));
+ }
+
+ /**
+ * 三候
+ *
+ * @return ThreePhenology 三候
+ */
+ function getThreePhenology(): ThreePhenology
+ {
+ return ThreePhenology::fromIndex($this->index % 3);
+ }
+
+}
diff --git a/src/culture/phenology/PhenologyDay.php b/src/culture/phenology/PhenologyDay.php
new file mode 100644
index 0000000..8cf879f
--- /dev/null
+++ b/src/culture/phenology/PhenologyDay.php
@@ -0,0 +1,29 @@
+culture;
+ }
+}
diff --git a/src/culture/phenology/ThreePhenology.php b/src/culture/phenology/ThreePhenology.php
new file mode 100644
index 0000000..af4c1db
--- /dev/null
+++ b/src/culture/phenology/ThreePhenology.php
@@ -0,0 +1,41 @@
+nextIndex($n));
+ }
+
+}
diff --git a/src/culture/star/nine/Dipper.php b/src/culture/star/nine/Dipper.php
new file mode 100644
index 0000000..85e32d4
--- /dev/null
+++ b/src/culture/star/nine/Dipper.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/star/nine/NineStar.php b/src/culture/star/nine/NineStar.php
new file mode 100644
index 0000000..bd75632
--- /dev/null
+++ b/src/culture/star/nine/NineStar.php
@@ -0,0 +1,87 @@
+nextIndex($n));
+ }
+
+ /**
+ * 颜色
+ *
+ * @return string 颜色
+ */
+ function getColor(): string
+ {
+ return ['白', '黒', '碧', '绿', '黄', '白', '赤', '白', '紫'][$this->index];
+ }
+
+ /**
+ * 五行
+ *
+ * @return Element 五行
+ */
+ function getElement(): Element
+ {
+ return Element::fromIndex([4, 2, 0, 0, 2, 3, 3, 2, 1][$this->index]);
+ }
+
+ /**
+ * 北斗九星
+ *
+ * @return Dipper 北斗九星
+ */
+ function getDipper(): Dipper
+ {
+ return Dipper::fromIndex($this->index);
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return Direction::fromIndex($this->index);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s%s', $this->getName(), $this->getColor(), $this->getElement());
+ }
+}
diff --git a/src/culture/star/seven/SevenStar.php b/src/culture/star/seven/SevenStar.php
new file mode 100644
index 0000000..b0714ef
--- /dev/null
+++ b/src/culture/star/seven/SevenStar.php
@@ -0,0 +1,51 @@
+nextIndex($n));
+ }
+
+ /**
+ * 星期
+ *
+ * @return Week 星期
+ */
+ function getWeek(): Week
+ {
+ return Week::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/star/ten/TenStar.php b/src/culture/star/ten/TenStar.php
new file mode 100644
index 0000000..3ee5d3d
--- /dev/null
+++ b/src/culture/star/ten/TenStar.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/culture/star/twelve/Ecliptic.php b/src/culture/star/twelve/Ecliptic.php
new file mode 100644
index 0000000..a028ed4
--- /dev/null
+++ b/src/culture/star/twelve/Ecliptic.php
@@ -0,0 +1,51 @@
+nextIndex($n));
+ }
+
+ /**
+ * 吉凶
+ *
+ * @return Luck 吉凶
+ */
+ function getLuck(): Luck
+ {
+ return Luck::fromIndex($this->index);
+ }
+}
diff --git a/src/culture/star/twelve/TwelveStar.php b/src/culture/star/twelve/TwelveStar.php
new file mode 100644
index 0000000..cb64b51
--- /dev/null
+++ b/src/culture/star/twelve/TwelveStar.php
@@ -0,0 +1,51 @@
+nextIndex($n));
+ }
+
+ /**
+ * 黄道黑道
+ *
+ * @return Ecliptic 黄道黑道
+ */
+ function getEcliptic(): Ecliptic
+ {
+ return Ecliptic::fromIndex([0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1][$this->index]);
+ }
+
+}
diff --git a/src/culture/star/twentyeight/TwentyEightStar.php b/src/culture/star/twentyeight/TwentyEightStar.php
new file mode 100644
index 0000000..631099d
--- /dev/null
+++ b/src/culture/star/twentyeight/TwentyEightStar.php
@@ -0,0 +1,96 @@
+nextIndex($n));
+ }
+
+ /**
+ * 七曜
+ *
+ * @return SevenStar 七曜
+ */
+ function getSevenStar(): SevenStar
+ {
+ return SevenStar::fromIndex($this->index % 7 + 4);
+ }
+
+ /**
+ * 九野
+ *
+ * @return Land 九野
+ */
+ function getLand(): Land
+ {
+ return Land::fromIndex([4, 4, 4, 2, 2, 2, 7, 7, 7, 0, 0, 0, 0, 5, 5, 5, 6, 6, 6, 1, 1, 1, 8, 8, 8, 3, 3, 3][$this->index]);
+ }
+
+ /**
+ * 宫
+ *
+ * @return Zone 宫
+ */
+ function getZone(): Zone
+ {
+ return Zone::fromIndex(intdiv($this->index, 7));
+ }
+
+ /**
+ * 动物
+ *
+ * @return Animal 动物
+ */
+ function getAnimal(): Animal
+ {
+ return Animal::fromIndex($this->index);
+ }
+
+ /**
+ * 吉凶
+ *
+ * @return Luck 吉凶
+ */
+ function getLuck(): Luck
+ {
+ return Luck::fromIndex([0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0][$this->index]);
+ }
+
+}
diff --git a/src/eightchar/ChildLimit.php b/src/eightchar/ChildLimit.php
new file mode 100644
index 0000000..e554b73
--- /dev/null
+++ b/src/eightchar/ChildLimit.php
@@ -0,0 +1,260 @@
+startTime = $birthTime;
+ $this->gender = $gender;
+ $this->eightChar = $birthTime->getLunarHour()->getEightChar();
+ // 阳男阴女顺推,阴男阳女逆推
+ $yang = YinYang::YANG == $this->eightChar->getYear()->getHeavenStem()->getYinYang();
+ $man = Gender::MAN == $gender;
+ $forward = ($yang && $man) || (!$yang && !$man);
+ $term = $birthTime->getTerm();
+ if (!$term->isJie()) {
+ $term = $term->next(-1);
+ }
+ $start = $forward ? $birthTime : $term->getJulianDay()->getSolarTime();
+ $end = $forward ? $term->next(2)->getJulianDay()->getSolarTime() : $birthTime;
+
+ $seconds = $end->subtract($start);
+ // 3天 = 1年,3天=60*60*24*3秒=259200秒 = 1年
+ $year = intdiv($seconds, 259200);
+ $seconds %= 259200;
+ // 1天 = 4月,1天=60*60*24秒=86400秒 = 4月,85400秒/4=21600秒 = 1月
+ $month = intdiv($seconds, 21600);
+ $seconds %= 21600;
+ // 1时 = 5天,1时=60*60秒=3600秒 = 5天,3600秒/5=720秒 = 1天
+ $day = intdiv($seconds, 720);
+ $seconds %= 720;
+ // 1分 = 2时,60秒 = 2时,60秒/2=30秒 = 1时
+ $hour = intdiv($seconds, 30);
+ $seconds %= 30;
+ // 1秒 = 2分,1秒/2=0.5秒 = 1分
+ $minute = $seconds * 2;
+
+ $this->forward = $forward;
+ $this->yearCount = $year;
+ $this->monthCount = $month;
+ $this->dayCount = $day;
+ $this->hourCount = $hour;
+ $this->minuteCount = $minute;
+
+ $birthday = $birthTime->getDay();
+ $birthMonth = $birthday->getMonth();
+
+ $d = $birthday->getDay() + $day;
+ $h = $birthTime->getHour() + $hour;
+ $mi = $birthTime->getMinute() + $minute;
+ $h += intdiv($mi, 60);
+ $mi %= 60;
+ $d += intdiv($h, 24);
+ $h %= 24;
+
+ $sm = SolarMonth::fromYm($birthMonth->getYear()->getYear() + $year, $birthMonth->getMonth())->next($month);
+
+ $dc = $sm->getDayCount();
+ if ($d > $dc) {
+ $d -= $dc;
+ $sm = $sm->next(1);
+ }
+ $this->endTime = SolarTime::fromYmdHms($sm->getYear()->getYear(), $sm->getMonth(), $d, $h, $mi, $birthTime->getSecond());
+ }
+
+ /**
+ * 通过出生公历时刻初始化
+ *
+ * @param SolarTime $birthTime 出生公历时刻
+ * @param Gender $gender 性别
+ * @return static 童限
+ */
+ static function fromSolarTime(SolarTime $birthTime, Gender $gender): static
+ {
+ return new static($birthTime, $gender);
+ }
+
+ /**
+ * 八字
+ *
+ * @return EightChar 八字
+ */
+ function getEightChar(): EightChar
+ {
+ return $this->eightChar;
+ }
+
+ /**
+ * 性别
+ *
+ * @return Gender 性别
+ */
+ function getGender(): Gender
+ {
+ return $this->gender;
+ }
+
+ /**
+ * 是否顺推
+ *
+ * @return bool true/false
+ */
+ function isForward(): bool
+ {
+ return $this->forward;
+ }
+
+ /**
+ * 年数
+ *
+ * @return int 年数
+ */
+ function getYearCount(): int
+ {
+ return $this->yearCount;
+ }
+
+ /**
+ * 月数
+ *
+ * @return int 月数
+ */
+ function getMonthCount(): int
+ {
+ return $this->monthCount;
+ }
+
+ /**
+ * 日数
+ *
+ * @return int 日数
+ */
+ function getDayCount(): int
+ {
+ return $this->dayCount;
+ }
+
+ /**
+ * 小时数
+ *
+ * @return int 小时数
+ */
+ function getHourCount(): int
+ {
+ return $this->hourCount;
+ }
+
+ /**
+ * 分钟数
+ *
+ * @return int 分钟数
+ */
+ function getMinuteCount(): int
+ {
+ return $this->minuteCount;
+ }
+
+ /**
+ * 开始(即出生)的公历时刻
+ *
+ * @return SolarTime 公历时刻
+ */
+ function getStartTime(): SolarTime
+ {
+ return $this->startTime;
+ }
+
+ /**
+ * 结束(即开始起运)的公历时刻
+ *
+ * @return SolarTime 公历时刻
+ */
+ function getEndTime(): SolarTime
+ {
+ return $this->endTime;
+ }
+
+ /**
+ * 大运
+ *
+ * @return DecadeFortune 大运
+ */
+ function getStartDecadeFortune(): DecadeFortune
+ {
+ return DecadeFortune::fromChildLimit($this, 0);
+ }
+
+ /**
+ * 小运
+ *
+ * @return Fortune 小运
+ */
+ function getStartFortune(): Fortune
+ {
+ return Fortune::fromChildLimit($this, 0);
+ }
+
+}
diff --git a/src/eightchar/DecadeFortune.php b/src/eightchar/DecadeFortune.php
new file mode 100644
index 0000000..d47837c
--- /dev/null
+++ b/src/eightchar/DecadeFortune.php
@@ -0,0 +1,109 @@
+childLimit = $childLimit;
+ $this->index = $index;
+ }
+
+ static function fromChildLimit(ChildLimit $childLimit, int $index): static
+ {
+ return new static($childLimit, $index);
+ }
+
+ /**
+ * 开始年龄
+ *
+ * @return int 开始年龄
+ */
+ function getStartAge(): int
+ {
+ return $this->childLimit->getYearCount() + 1 + $this->index * 10;
+ }
+
+ /**
+ * 结束年龄
+ *
+ * @return int 结束年龄
+ */
+ function getEndAge(): int
+ {
+ return $this->getStartAge() + 9;
+ }
+
+ /**
+ * 开始农历年
+ *
+ * @return LunarYear 农历年
+ */
+ function getStartLunarYear(): LunarYear
+ {
+ return $this->childLimit->getEndTime()->getLunarHour()->getDay()->getMonth()->getYear()->next($this->index * 10);
+ }
+
+ /**
+ * 结束农历年
+ *
+ * @return LunarYear 农历年
+ */
+ function getEndLunarYear(): LunarYear
+ {
+ return $this->getStartLunarYear()->next(9);
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ $n = $this->index + 1;
+ return $this->childLimit->getEightChar()->getMonth()->next($this->childLimit->isForward() ? $n : -$n);
+ }
+
+ function getName(): string
+ {
+ return $this->getSixtyCycle()->getName();
+ }
+
+ function next(int $n): static
+ {
+ return self::fromChildLimit($this->childLimit, $this->index + $n);
+ }
+
+ /**
+ * 开始小运
+ *
+ * @return Fortune 小运
+ */
+ function getStartFortune(): Fortune
+ {
+ return Fortune::fromChildLimit($this->childLimit, $this->index * 10);
+ }
+
+}
diff --git a/src/eightchar/EightChar.php b/src/eightchar/EightChar.php
new file mode 100644
index 0000000..212d58a
--- /dev/null
+++ b/src/eightchar/EightChar.php
@@ -0,0 +1,148 @@
+year = $year;
+ $this->month = $month;
+ $this->day = $day;
+ $this->hour = $hour;
+ }
+
+ /**
+ * 年柱
+ *
+ * @return SixtyCycle 年柱
+ */
+ function getYear(): SixtyCycle
+ {
+ return $this->year;
+ }
+
+ /**
+ * 月柱
+ *
+ * @return SixtyCycle 月柱
+ */
+ function getMonth(): SixtyCycle
+ {
+ return $this->month;
+ }
+
+ /**
+ * 日柱
+ *
+ * @return SixtyCycle 日柱
+ */
+ function getDay(): SixtyCycle
+ {
+ return $this->day;
+ }
+
+ /**
+ * 时柱
+ *
+ * @return SixtyCycle 时柱
+ */
+ function getHour(): SixtyCycle
+ {
+ return $this->hour;
+ }
+
+ /**
+ * 胎元
+ *
+ * @return SixtyCycle 胎元
+ */
+ function getFetalOrigin(): SixtyCycle
+ {
+ return SixtyCycle::fromName(sprintf('%s%s', $this->month->getHeavenStem()->next(1)->getName(), $this->month->getEarthBranch()->next(3)->getName()));
+ }
+
+ /**
+ * 胎息
+ *
+ * @return SixtyCycle 胎息
+ */
+ function getFetalBreath(): SixtyCycle
+ {
+ return SixtyCycle::fromName(sprintf('%s%s', $this->day->getHeavenStem()->next(5)->getName(), EarthBranch::fromIndex(13 - $this->day->getEarthBranch()->getIndex())->getName()));
+ }
+
+ /**
+ * 命宫
+ *
+ * @return SixtyCycle 命宫
+ */
+ function getOwnSign(): SixtyCycle
+ {
+ $offset = $this->month->getEarthBranch()->next(-1)->getIndex() + $this->hour->getEarthBranch()->next(-1)->getIndex();
+ $offset = ($offset >= 14 ? 26 : 14) - $offset;
+ $offset -= 1;
+ return SixtyCycle::fromName(sprintf('%s%s', HeavenStem::fromIndex(($this->year->getHeavenStem()->getIndex() + 1) * 2 + $offset)->getName(), EarthBranch::fromIndex(2 + $offset)->getName()));
+ }
+
+ /**
+ * 身宫
+ *
+ * @return SixtyCycle 身宫
+ */
+ function getBodySign(): SixtyCycle
+ {
+ $offset = $this->month->getEarthBranch()->getIndex() + $this->hour->getEarthBranch()->getIndex();
+ $offset %= 12;
+ $offset -= 1;
+ return SixtyCycle::fromName(sprintf('%s%s', HeavenStem::fromIndex(($this->year->getHeavenStem()->getIndex() + 1) * 2 + $offset)->getName(), EarthBranch::fromIndex(2 + $offset)->getName()));
+ }
+
+ /**
+ * 建除十二值神
+ *
+ * @return Duty 建除十二值神
+ */
+ function getDuty(): Duty
+ {
+ return Duty::fromIndex($this->day->getEarthBranch()->getIndex() - $this->month->getEarthBranch()->getIndex());
+ }
+
+ function getName(): string
+ {
+ return sprintf('%s %s %s %s', $this->year, $this->month, $this->day, $this->hour);
+ }
+
+}
diff --git a/src/eightchar/Fortune.php b/src/eightchar/Fortune.php
new file mode 100644
index 0000000..a72ed26
--- /dev/null
+++ b/src/eightchar/Fortune.php
@@ -0,0 +1,79 @@
+childLimit = $childLimit;
+ $this->index = $index;
+ }
+
+ static function fromChildLimit(ChildLimit $childLimit, int $index): static
+ {
+ return new static($childLimit, $index);
+ }
+
+ /**
+ * 年龄
+ *
+ * @return int 年龄
+ */
+ function getAge(): int
+ {
+ return $this->childLimit->getYearCount() + 1 + $this->index;
+ }
+
+ /**
+ * 农历年
+ *
+ * @return LunarYear 农历年
+ */
+ function getLunarYear(): LunarYear
+ {
+ return $this->childLimit->getEndTime()->getLunarHour()->getDay()->getMonth()->getYear()->next($this->index);
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ $n = $this->getAge();
+ return $this->childLimit->getEightChar()->getHour()->next($this->childLimit->isForward() ? $n : -$n);
+ }
+
+ function getName(): string
+ {
+ return $this->getSixtyCycle()->getName();
+ }
+
+ function next(int $n): static
+ {
+ return self::fromChildLimit($this->childLimit, $this->index + $n);
+ }
+
+}
diff --git a/src/enums/FestivalType.php b/src/enums/FestivalType.php
new file mode 100644
index 0000000..414e984
--- /dev/null
+++ b/src/enums/FestivalType.php
@@ -0,0 +1,55 @@
+value;
+ }
+
+ function getName(): string
+ {
+ return match ($this) {
+ self::DAY => '日期',
+ self::TERM => '节气',
+ self::EVE => '除夕'
+ };
+ }
+
+ static function fromCode(int $code): FestivalType
+ {
+ return match (true) {
+ $code == 0 => self::DAY,
+ $code == 1 => self::TERM,
+ $code == 2 => self::EVE,
+ default => null
+ };
+ }
+
+ static function fromName(string $name): FestivalType
+ {
+ return match (true) {
+ $name == '日期' => self::DAY,
+ $name == '节气' => self::TERM,
+ $name == '除夕' => self::EVE,
+ default => null
+ };
+ }
+
+ function equals(Side $o): bool
+ {
+ return $this->value == $o->value;
+ }
+
+}
diff --git a/src/enums/Gender.php b/src/enums/Gender.php
new file mode 100644
index 0000000..ca3606f
--- /dev/null
+++ b/src/enums/Gender.php
@@ -0,0 +1,51 @@
+value;
+ }
+
+ function getName(): string
+ {
+ return match ($this) {
+ self::WOMAN => '女',
+ self::MAN => '男'
+ };
+ }
+
+ static function fromCode(int $code): Gender
+ {
+ return match (true) {
+ $code == 0 => self::WOMAN,
+ $code == 1 => self::MAN,
+ default => null
+ };
+ }
+
+ static function fromName(string $name): Gender
+ {
+ return match (true) {
+ $name == '女' => self::WOMAN,
+ $name == '男' => self::MAN,
+ default => null
+ };
+ }
+
+ function equals(Side $o): bool
+ {
+ return $this->value == $o->value;
+ }
+
+}
diff --git a/src/enums/Side.php b/src/enums/Side.php
new file mode 100644
index 0000000..5494902
--- /dev/null
+++ b/src/enums/Side.php
@@ -0,0 +1,51 @@
+value;
+ }
+
+ function getName(): string
+ {
+ return match ($this) {
+ self::IN => '内',
+ self::OUT => '外'
+ };
+ }
+
+ static function fromCode(int $code): Side
+ {
+ return match (true) {
+ $code == 0 => self::IN,
+ $code == 1 => self::OUT,
+ default => null
+ };
+ }
+
+ static function fromName(string $name): Side
+ {
+ return match (true) {
+ $name == '内' => self::IN,
+ $name == '外' => self::OUT,
+ default => null
+ };
+ }
+
+ function equals(Side $o): bool
+ {
+ return $this->value == $o->value;
+ }
+
+}
diff --git a/src/enums/YinYang.php b/src/enums/YinYang.php
new file mode 100644
index 0000000..ce36634
--- /dev/null
+++ b/src/enums/YinYang.php
@@ -0,0 +1,51 @@
+value;
+ }
+
+ function getName(): string
+ {
+ return match ($this) {
+ self::YIN => '阴',
+ self::YANG => '阳'
+ };
+ }
+
+ static function fromCode(int $code): YinYang
+ {
+ return match (true) {
+ $code == 1 => self::YANG,
+ $code == 0 => self::YIN,
+ default => null
+ };
+ }
+
+ static function fromName(string $name): YinYang
+ {
+ return match (true) {
+ $name == '阳' => self::YANG,
+ $name == '阴' => self::YIN,
+ default => null
+ };
+ }
+
+ function equals(YinYang $o): bool
+ {
+ return $this->value == $o->value;
+ }
+
+}
diff --git a/src/festival/LunarFestival.php b/src/festival/LunarFestival.php
new file mode 100644
index 0000000..13f5a1f
--- /dev/null
+++ b/src/festival/LunarFestival.php
@@ -0,0 +1,164 @@
+type = $type;
+ $this->day = $day;
+ $this->solarTerm = $solarTerm;
+ $this->index = intval(substr($data, 1, 2));
+ $this->name = static::$NAMES[$this->index];
+ }
+
+ static function fromIndex(int $year, int $index): ?static
+ {
+ if ($index < 0 || $index >= count(static::$NAMES)) {
+ throw new InvalidArgumentException(sprintf('illegal index: %d', $index));
+ }
+ if (preg_match_all(sprintf('/@%02d\\d+/', $index), static::$DATA, $matches)) {
+ $data = $matches[0][0];
+ $type = FestivalType::fromCode(ord(substr($data, 3, 1)) - 48);
+ switch ($type) {
+ case FestivalType::DAY:
+ return new static($type, LunarDay::fromYmd($year, intval(substr($data, 4, 2)), intval(substr($data, 6, 2))), null, $data);
+ case FestivalType::TERM:
+ $solarTerm = SolarTerm::fromIndex($year, intval(substr($data, 4, 2)));
+ return new static($type, $solarTerm->getJulianDay()->getSolarDay()->getLunarDay(), $solarTerm, $data);
+ case FestivalType::EVE:
+ return new static($type, LunarDay::fromYmd($year + 1, 1, 1)->next(-1), null, $data);
+ }
+ }
+ return null;
+ }
+
+ static function fromYmd(int $year, int $month, int $day): ?static
+ {
+ if (preg_match_all(sprintf('/@\d{2}0%02d%02d/', $month, $day), static::$DATA, $matches)) {
+ return new static(FestivalType::DAY, LunarDay::fromYmd($year, $month, $day), null, $matches[0][0]);
+ }
+ if (preg_match_all('/@\\d{2}1\\d{2}/', static::$DATA, $matches)) {
+ $data = $matches[0][0];
+ $solarTerm = SolarTerm::fromIndex($year, intval(substr($data, 4, 2)));
+ $lunarDay = $solarTerm->getJulianDay()->getSolarDay()->getLunarDay();
+ $lunarMonth = $lunarDay->getMonth();
+ if ($lunarMonth->getYear()->getYear() == $year && $lunarMonth->getMonth() == $month && $lunarDay->getDay() == $day) {
+ return new static(FestivalType::TERM, $lunarDay, $solarTerm, $data);
+ }
+ }
+ if (preg_match_all('/@\\d{2}2/', static::$DATA, $matches)) {
+ $lunarDay = LunarDay::fromYmd($year, $month, $day);
+ $nextDay = $lunarDay->next(1);
+ if ($nextDay->getMonth()->getMonth() == 1 && $nextDay->getDay() == 1) {
+ return new static(FestivalType::EVE, $lunarDay, null, $matches[0][0]);
+ }
+ }
+ return null;
+ }
+
+ function next(int $n): static
+ {
+ $m = $this->day->getMonth();
+ $year = $m->getYear()->getYear();
+ if ($n == 0) {
+ return static::fromYmd($year, $m->getMonthWithLeap(), $this->day->getDay());
+ }
+ $size = count(self::$NAMES);
+ $t = $this->index + $n;
+ $offset = $this->indexOf($t, null, $size);
+ if ($t < 0) {
+ $t -= $size;
+ }
+ return static::fromIndex($year + intdiv($t, $size), $offset);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s %s', $this->day, $this->name);
+ }
+
+ /**
+ * 类型
+ * @return FestivalType 节日类型
+ */
+ function getType(): FestivalType
+ {
+ return $this->type;
+ }
+
+ /**
+ * @return LunarDay 农历日
+ */
+ function getDay(): LunarDay
+ {
+ return $this->day;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 节气,非节气返回null
+ *
+ * @return SolarTerm 节气
+ */
+ function getSolarTerm(): SolarTerm
+ {
+ return $this->solarTerm;
+ }
+}
diff --git a/src/festival/SolarFestival.php b/src/festival/SolarFestival.php
new file mode 100644
index 0000000..518ca6a
--- /dev/null
+++ b/src/festival/SolarFestival.php
@@ -0,0 +1,149 @@
+type = $type;
+ $this->day = $day;
+ $this->startYear = $startYear;
+ $this->index = intval(substr($data, 1, 2));
+ $this->name = static::$NAMES[$this->index];
+ }
+
+ static function fromIndex(int $year, int $index): ?static
+ {
+ if ($index < 0 || $index >= count(static::$NAMES)) {
+ throw new InvalidArgumentException(sprintf('illegal index: %d', $index));
+ }
+ if(preg_match_all(sprintf('/@%02d\\d+/', $index), static::$DATA, $matches)) {
+ $data = $matches[0][0];
+ $type = FestivalType::fromCode(ord(substr($data, 3, 1)) - 48);
+ if ($type == FestivalType::DAY) {
+ $startYear = intval(substr($data, 8, 4));
+ if ($year >= $startYear) {
+ return new static($type, SolarDay::fromYmd($year, intval(substr($data, 4, 2)), intval(substr($data, 6, 2))), $startYear, $data);
+ }
+ }
+ }
+ return null;
+ }
+
+ static function fromYmd(int $year, int $month, int $day): ?static
+ {
+ if (preg_match_all(sprintf('/@\\d{2}0%02d%02d\\d+/', $month, $day), static::$DATA, $matches)) {
+ $data = $matches[0][0];
+ $startYear = intval(substr($data, 8, 4));
+ if ($year >= $startYear) {
+ return new static(FestivalType::DAY, SolarDay::fromYmd($year, $month, $day), $startYear, $data);
+ }
+ }
+ return null;
+ }
+
+ function next(int $n): static
+ {
+ $m = $this->day->getMonth();
+ $year = $m->getYear()->getYear();
+ if ($n == 0) {
+ return static::fromYmd($year, $m->getMonth(), $this->day->getDay());
+ }
+ $size = count(static::$NAMES);
+ $t = $this->index + $n;
+ $offset = $this->indexOf($t, null, $size);
+ if ($t < 0) {
+ $t -= $size;
+ }
+ return static::fromIndex($year + intdiv($t, $size), $offset);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s %s', $this->day, $this->name);
+ }
+
+ /**
+ * 类型
+ * @return FestivalType 节日类型
+ */
+ function getType(): FestivalType
+ {
+ return $this->type;
+ }
+
+ /**
+ * 公历日
+ * @return SolarDay 公历日
+ */
+ function getDay(): SolarDay
+ {
+ return $this->day;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 起始年
+ *
+ * @return int 年
+ */
+ function getStartYear(): int
+ {
+ return $this->startYear;
+ }
+}
diff --git a/src/holiday/LegalHoliday.php b/src/holiday/LegalHoliday.php
new file mode 100644
index 0000000..bd38f93
--- /dev/null
+++ b/src/holiday/LegalHoliday.php
@@ -0,0 +1,131 @@
+day = SolarDay::fromYmd($year, $month, $day);
+ $this->work = '0' == substr($data, 8, 1);
+ $this->name = static::$NAMES[ord(substr($data, 9, 1)) - 48];
+ }
+
+ static function fromYmd(int $year, int $month, int $day): ?static
+ {
+ if(preg_match_all(sprintf('/%04d%02d%02d[0-1][0-8][\\+|-]\\d{2}/', $year, $month, $day), static::$DATA, $matches)) {
+ return new static($year, $month, $day, $matches[0][0]);
+ }
+ return null;
+ }
+
+ function next(int $n): ?static
+ {
+ $m = $this->day->getMonth();
+ $year = $m->getYear()->getYear();
+ $month = $m->getMonth();
+ if ($n == 0) {
+ return static::fromYmd($year, $month, $this->day->getDay());
+ }
+ $reg = '/%04d\\d{4}[0-1][0-8][\\+|-]\\d{2}/';
+ $today = sprintf('%04d%02d%02d', $year, $month, $this->day->getDay());
+ $index = -1;
+ $size = 0;
+ if (preg_match_all(sprintf($reg, $year), static::$DATA, $matches)) {
+ $size = count($matches[0]);
+ for ($i = 0; $i < $size; $i++) {
+ if (str_starts_with($matches[0][$i], $today)) {
+ $index = $i;
+ break;
+ }
+ }
+ }
+ if ($index == -1) {
+ return null;
+ }
+ $index += $n;
+ $y = $year;
+ $forward = $n > 0;
+ $add = $forward ? 1 : -1;
+ while ($forward ? ($index >= $size) : ($index < 0)) {
+ if ($forward) {
+ $index -= $size;
+ }
+ $y += $add;
+ $size = 0;
+ if(preg_match_all(sprintf($reg, $y), static::$DATA, $matches)) {
+ $size = count($matches[0]);
+ }
+ if ($size < 1) {
+ return null;
+ }
+ if (!$forward) {
+ $index += $size;
+ }
+ }
+ $d = $matches[0][$index];
+ return new static(intval(substr($d, 0, 4)), intval(substr($d, 4, 2)), intval(substr($d, 6, 2)), $d);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s %s(%s)', $this->day, $this->name, $this->work ? '班' : '休');
+ }
+
+ function getDay(): SolarDay
+ {
+ return $this->day;
+ }
+
+ function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否上班
+ *
+ * @return bool true/false
+ */
+ function isWork(): bool
+ {
+ return $this->work;
+ }
+
+ /**
+ * @param mixed $o 对象
+ * @return bool true/false
+ */
+ function equals(mixed $o): bool
+ {
+ return $o instanceof LegalHoliday && $this->__toString() == $o->__toString();
+ }
+}
diff --git a/src/jd/JulianDay.php b/src/jd/JulianDay.php
new file mode 100644
index 0000000..0c5f44d
--- /dev/null
+++ b/src/jd/JulianDay.php
@@ -0,0 +1,186 @@
+day = $day;
+ }
+
+ static function fromJulianDay($day): static
+ {
+ return new static($day);
+ }
+
+ static function fromYmdHms(int $year, int $month, int $day, int $hour, int $minute, int $second): static
+ {
+ $d = $day + (($second / 60 + $minute) / 60 + $hour) / 24;
+ $n = 0;
+ $g = $year * 372 + $month * 31 + (int)$d >= 588829;
+ if ($month <= 2) {
+ $month += 12;
+ $year--;
+ }
+ if ($g) {
+ $n = intdiv($year, 100);
+ $n = 2 - $n + intdiv($n, 4);
+ }
+ return static::fromJulianDay((int)(365.25 * ($year + 4716)) + (int)(30.6001 * ($month + 1)) + $d + $n - 1524.5);
+ }
+
+ /**
+ * 儒略日
+ *
+ * @return float 儒略日
+ */
+ function getDay(): float
+ {
+ return $this->day;
+ }
+
+ function getName(): string
+ {
+ return $this->day . '';
+ }
+
+ function next(int $n): static
+ {
+ return static::fromJulianDay($this->day + $n);
+ }
+
+ /**
+ * 公历日
+ *
+ * @return SolarDay 公历日
+ */
+ function getSolarDay(): SolarDay
+ {
+ $d = (int)($this->day + 0.5);
+ $f = $this->day + 0.5 - $d;
+
+ if ($d >= 2299161) {
+ $c = (int)(($d - 1867216.25) / 36524.25);
+ $d += 1 + $c - intdiv($c, 4);
+ }
+ $d += 1524;
+ $year = (int)(($d - 122.1) / 365.25);
+ $d -= (int)(365.25 * $year);
+ $month = (int)($d / 30.601);
+ $d -= (int)(30.601 * $month);
+ $day = $d;
+ if ($month > 13) {
+ $month -= 13;
+ $year -= 4715;
+ } else {
+ $month -= 1;
+ $year -= 4716;
+ }
+ $f *= 24;
+ $hour = (int)$f;
+
+ $f -= $hour;
+ $f *= 60;
+ $minute = (int)$f;
+
+ $f -= $minute;
+ $f *= 60;
+ $second = (int)(round($f));
+ if ($second > 59) {
+ $minute++;
+ }
+ if ($minute > 59) {
+ $hour++;
+ }
+ if ($hour > 23) {
+ $day += 1;
+ }
+ return SolarDay::fromYmd($year, $month, $day);
+ }
+
+ /**
+ * 公历时刻
+ *
+ * @return SolarTime 公历时刻
+ */
+ function getSolarTime(): SolarTime
+ {
+ $d = (int)($this->day + 0.5);
+ $f = $this->day + 0.5 - $d;
+
+ if ($d >= 2299161) {
+ $c = (int)(($d - 1867216.25) / 36524.25);
+ $d += 1 + $c - intdiv($c, 4);
+ }
+ $d += 1524;
+ $year = (int)(($d - 122.1) / 365.25);
+ $d -= (int)(365.25 * $year);
+ $month = (int)($d / 30.601);
+ $d -= (int)(30.601 * $month);
+ $day = $d;
+ if ($month > 13) {
+ $month -= 13;
+ $year -= 4715;
+ } else {
+ $month -= 1;
+ $year -= 4716;
+ }
+ $f *= 24;
+ $hour = (int)$f;
+
+ $f -= $hour;
+ $f *= 60;
+ $minute = (int)$f;
+
+ $f -= $minute;
+ $f *= 60;
+ $second = (int)round($f);
+ if ($second > 59) {
+ $second -= 60;
+ $minute++;
+ }
+ if ($minute > 59) {
+ $minute -= 60;
+ $hour++;
+ }
+ if ($hour > 23) {
+ $hour -= 24;
+ $day += 1;
+ }
+ return SolarTime::fromYmdHms($year, $month, $day, $hour, $minute, $second);
+ }
+
+ /**
+ * 星期
+ *
+ * @return Week 星期
+ */
+ function getWeek(): Week
+ {
+ return Week::fromIndex((int)($this->day + 0.5) + 7000001);
+ }
+
+
+}
diff --git a/src/lunar/LunarDay.php b/src/lunar/LunarDay.php
new file mode 100644
index 0000000..8543bf2
--- /dev/null
+++ b/src/lunar/LunarDay.php
@@ -0,0 +1,349 @@
+ $m->getDayCount()) {
+ throw new InvalidArgumentException(sprintf('illegal day %d in %s', $day, $m));
+ }
+ $this->month = $m;
+ $this->day = $day;
+ }
+
+ static function fromYmd(int $year, int $month, int $day): static
+ {
+ return new static($year, $month, $day);
+ }
+
+ /**
+ * 月
+ *
+ * @return LunarMonth 月
+ */
+ function getMonth(): LunarMonth
+ {
+ return $this->month;
+ }
+
+ /**
+ * 日
+ *
+ * @return int 日
+ */
+ function getDay(): int
+ {
+ return $this->day;
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->day - 1];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->month, $this->getName());
+ }
+
+ function next(int $n): LunarDay
+ {
+ if ($n == 0) {
+ return self::fromYmd($this->month->getYear()->getYear(), $this->month->getMonthWithLeap(), $this->day);
+ }
+ $d = $this->day + $n;
+ $lm = $this->month;
+ $daysInMonth = $lm->getDayCount();
+ $forward = $n > 0;
+ $add = $forward ? 1 : -1;
+ while ($forward ? ($d > $daysInMonth) : ($d <= 0)) {
+ if ($forward) {
+ $d -= $daysInMonth;
+ }
+ $lm = $lm->next($add);
+ $daysInMonth = $lm->getDayCount();
+ if (!$forward) {
+ $d += $daysInMonth;
+ }
+ }
+ return self::fromYmd($lm->getYear()->getYear(), $lm->getMonthWithLeap(), $d);
+ }
+
+ /**
+ * 是否在指定农历日之前
+ *
+ * @param LunarDay target 农历日
+ * @return bool true/false
+ */
+ function isBefore(LunarDay $target): bool
+ {
+ $aYear = $this->month->getYear()->getYear();
+ $targetMonth = $target->getMonth();
+ $bYear = $targetMonth->getYear()->getYear();
+ if ($aYear == $bYear) {
+ $aMonth = $this->month->getMonth();
+ $bMonth = $targetMonth->getMonth();
+ if ($aMonth == $bMonth) {
+ if ($this->month->isLeap() && !$targetMonth->isLeap()) {
+ return false;
+ }
+ return $this->day < $target->getDay();
+ }
+ return $aMonth < $bMonth;
+ }
+ return $aYear < $bYear;
+ }
+
+ /**
+ * 是否在指定农历日之后
+ *
+ * @param LunarDay target 农历日
+ * @return bool true/false
+ */
+ function isAfter(LunarDay $target): bool
+ {
+ $aYear = $this->month->getYear()->getYear();
+ $targetMonth = $target->getMonth();
+ $bYear = $targetMonth->getYear()->getYear();
+ if ($aYear == $bYear) {
+ $aMonth = $this->month->getMonth();
+ $bMonth = $targetMonth->getMonth();
+ if ($aMonth == $bMonth) {
+ if ($this->month->isLeap() && !$targetMonth->isLeap()) {
+ return true;
+ }
+ return $this->day > $target->getDay();
+ }
+ return $aMonth > $bMonth;
+ }
+ return $aYear > $bYear;
+ }
+
+ /**
+ * 星期
+ *
+ * @return Week 星期
+ */
+ function getWeek(): Week
+ {
+ return $this->getSolarDay()->getJulianDay()->getWeek();
+ }
+
+ /**
+ * 当天的年干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getYearSixtyCycle(): SixtyCycle
+ {
+ $solarDay = $this->getSolarDay();
+ $solarYear = $solarDay->getMonth()->getYear()->getYear();
+ $springSolarDay = SolarTerm::fromIndex($solarYear, 3)->getJulianDay()->getSolarDay();
+ $lunarYear = $this->month->getYear();
+ $year = $lunarYear->getYear();
+ $sixtyCycle = $lunarYear->getSixtyCycle();
+ if ($year == $solarYear) {
+ if ($solarDay->isBefore($springSolarDay)) {
+ $sixtyCycle = $sixtyCycle->next(-1);
+ }
+ } else if ($year < $solarYear) {
+ if (!$solarDay->isBefore($springSolarDay)) {
+ $sixtyCycle = $sixtyCycle->next(1);
+ }
+ }
+ return $sixtyCycle;
+ }
+
+ /**
+ * 当天的月干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getMonthSixtyCycle(): SixtyCycle
+ {
+ $solarDay = $this->getSolarDay();
+ $year = $solarDay->getMonth()->getYear()->getYear();
+ $term = $solarDay->getTerm();
+ $index = $term->getIndex() - 3;
+ if ($index < 0 && $term->getJulianDay()->getSolarDay()->isAfter(SolarTerm::fromIndex($year, 3)->getJulianDay()->getSolarDay())) {
+ $index += 24;
+ }
+ return LunarMonth::fromYm($year, 1)->getSixtyCycle()->next((int)floor($index / 2));
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ $offset = (int)$this->month->getFirstJulianDay()->next($this->day - 12)->getDay();
+ return SixtyCycle::fromName(sprintf('%s%s', HeavenStem::fromIndex($offset)->getName(), EarthBranch::fromIndex($offset)->getName()));
+ }
+
+ /**
+ * 建除十二值神
+ *
+ * @return Duty 建除十二值神
+ */
+ function getDuty(): Duty
+ {
+ return Duty::fromIndex($this->getSixtyCycle()->getEarthBranch()->getIndex() - $this->getMonthSixtyCycle()->getEarthBranch()->getIndex());
+ }
+
+ /**
+ * 黄道黑道十二神
+ *
+ * @return TwelveStar 黄道黑道十二神
+ */
+ function getTwelveStar(): TwelveStar
+ {
+ return TwelveStar::fromIndex($this->getSixtyCycle()->getEarthBranch()->getIndex() + (8 - $this->getMonthSixtyCycle()->getEarthBranch()->getIndex() % 6) * 2);
+ }
+
+ /**
+ * 九星
+ *
+ * @return NineStar 九星
+ */
+ function getNineStar(): NineStar
+ {
+ $solar = $this->getSolarDay();
+ $dongZhi = SolarTerm::fromIndex($solar->getMonth()->getYear()->getYear(), 0);
+ $xiaZhi = $dongZhi->next(12);
+ $dongZhi2 = $dongZhi->next(24);
+ $dongZhiSolar = $dongZhi->getJulianDay()->getSolarDay();
+ $xiaZhiSolar = $xiaZhi->getJulianDay()->getSolarDay();
+ $dongZhiSolar2 = $dongZhi2->getJulianDay()->getSolarDay();
+ $dongZhiIndex = $dongZhiSolar->getLunarDay()->getSixtyCycle()->getIndex();
+ $xiaZhiIndex = $xiaZhiSolar->getLunarDay()->getSixtyCycle()->getIndex();
+ $dongZhiIndex2 = $dongZhiSolar2->getLunarDay()->getSixtyCycle()->getIndex();
+ $solarShunBai = $dongZhiSolar->next($dongZhiIndex > 29 ? 60 - $dongZhiIndex : -$dongZhiIndex);
+ $solarShunBai2 = $dongZhiSolar2->next($dongZhiIndex2 > 29 ? 60 - $dongZhiIndex2 : -$dongZhiIndex2);
+ $solarNiZi = $xiaZhiSolar->next($xiaZhiIndex > 29 ? 60 - $xiaZhiIndex : -$xiaZhiIndex);
+ $offset = 0;
+ if (!$solar->isBefore($solarShunBai) && $solar->isBefore($solarNiZi)) {
+ $offset = $solar->subtract($solarShunBai);
+ } else if (!$solar->isBefore($solarNiZi) && $solar->isBefore($solarShunBai2)) {
+ $offset = 8 - $solar->subtract($solarNiZi);
+ } else if (!$solar->isBefore($solarShunBai2)) {
+ $offset = $solar->subtract($solarShunBai2);
+ } else if ($solar->isBefore($solarShunBai)) {
+ $offset = 8 + $solarShunBai->subtract($solar);
+ }
+ return NineStar::fromIndex($offset);
+ }
+
+ /**
+ * 太岁方位
+ *
+ * @return Direction 方位
+ */
+ function getJupiterDirection(): Direction
+ {
+ $index = $this->getSixtyCycle()->getIndex();
+ if ($index % 12 < 6) {
+ return Direction::fromIndex([2, 8, 4, 6, 0][intdiv($index, 12)]);
+ }
+ return $this->month->getYear()->getJupiterDirection();
+ }
+
+ /**
+ * 逐日胎神
+ *
+ * @return FetusDay 逐日胎神
+ */
+ function getFetusDay(): FetusDay
+ {
+ return FetusDay::fromLunarDay($this);
+ }
+
+ /**
+ * 月相
+ *
+ * @return Phase 月相
+ */
+ function getPhase(): Phase
+ {
+ return Phase::fromIndex($this->day - 1);
+ }
+
+ /**
+ * 公历日
+ *
+ * @return SolarDay 公历日
+ */
+ function getSolarDay(): SolarDay
+ {
+ return $this->month->getFirstJulianDay()->next($this->day - 1)->getSolarDay();
+ }
+
+ /**
+ * 二十八宿
+ *
+ * @return TwentyEightStar 二十八宿
+ */
+ function getTwentyEightStar(): TwentyEightStar
+ {
+ return TwentyEightStar::fromIndex([10, 18, 26, 6, 14, 22, 2][$this->getSolarDay()->getWeek()->getIndex()])->next(-7 * $this->getSixtyCycle()->getEarthBranch()->getIndex());
+ }
+
+ /**
+ * 农历传统节日,如果当天不是农历传统节日,返回null
+ *
+ * @return ?LunarFestival 农历传统节日
+ */
+ function getFestival(): ?LunarFestival
+ {
+ $m = $this->getMonth();
+ return LunarFestival::fromYmd($m->getYear()->getYear(), $m->getMonthWithLeap(), $this->day);
+ }
+
+ function equals(mixed $o): bool
+ {
+ if (!($o instanceof LunarDay)) {
+ return false;
+ }
+ return $this->month->equals($o->getMonth()) && $this->day == $o->getDay();
+ }
+
+}
diff --git a/src/lunar/LunarHour.php b/src/lunar/LunarHour.php
new file mode 100644
index 0000000..e3862a4
--- /dev/null
+++ b/src/lunar/LunarHour.php
@@ -0,0 +1,287 @@
+ 23) {
+ throw new InvalidArgumentException(sprintf('illegal hour: %d', $hour));
+ }
+ if ($minute < 0 || $minute > 59) {
+ throw new InvalidArgumentException(sprintf('illegal minute: %d', $minute));
+ }
+ if ($second < 0 || $second > 59) {
+ throw new InvalidArgumentException(sprintf('illegal second: %d', $second));
+ }
+ $this->day = LunarDay::fromYmd($year, $month, $day);
+ $this->hour = $hour;
+ $this->minute = $minute;
+ $this->second = $second;
+ }
+
+ static function fromYmdHms(int $year, int $month, int $day, int $hour, int $minute, int $second): static
+ {
+ return new static($year, $month, $day, $hour, $minute, $second);
+ }
+
+ /**
+ * 农历日
+ *
+ * @return LunarDay 农历日
+ */
+ function getDay(): LunarDay
+ {
+ return $this->day;
+ }
+
+ /**
+ * 时
+ *
+ * @return int 时
+ */
+ function getHour(): int
+ {
+ return $this->hour;
+ }
+
+ /**
+ * 分
+ *
+ * @return int 分
+ */
+ function getMinute(): int
+ {
+ return $this->minute;
+ }
+
+ /**
+ * 秒
+ *
+ * @return int 秒
+ */
+ function getSecond(): int
+ {
+ return $this->second;
+ }
+
+ function getName(): string
+ {
+ return sprintf('%s时', EarthBranch::fromIndex($this->getIndexInDay())->getName());
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s时', $this->day, $this->getSixtyCycle()->getName());
+ }
+
+ function getIndexInDay(): int
+ {
+ return intdiv($this->hour + 1, 2);
+ }
+
+ /**
+ * 是否在指定农历时辰之前
+ *
+ * @param LunarHour $target 农历时辰
+ * @return bool true/false
+ */
+ function isBefore(LunarHour $target): bool
+ {
+ if (!$this->day->equals($target->getDay())) {
+ return $this->day->isBefore($target->getDay());
+ }
+ $bHour = $target->getHour();
+ if ($this->hour == $bHour) {
+ $bMinute = $target->getMinute();
+ return $this->minute == $bMinute ? $this->second < $target->getSecond() : $this->minute < $bMinute;
+ }
+ return $this->hour < $bHour;
+ }
+
+ /**
+ * 是否在指定农历时辰之后
+ *
+ * @param LunarHour $target 农历时辰
+ * @return true/false
+ */
+ function isAfter(LunarHour $target): bool
+ {
+ if (!$this->day->equals($target->getDay())) {
+ return $this->day->isAfter($target->getDay());
+ }
+ $bHour = $target->getHour();
+ if ($this->hour == $bHour) {
+ $bMinute = $target->getMinute();
+ return $this->minute == $bMinute ? $this->second > $target->getSecond() : $this->minute > $bMinute;
+ }
+ return $this->hour > $bHour;
+ }
+
+ function next(int $n): LunarHour
+ {
+ $h = $this->hour + $n * 2;
+ $diff = $h < 0 ? -1 : 1;
+ $hour = abs($h);
+ $days = intdiv($hour, 24) * $diff;
+ $hour = ($hour % 24) * $diff;
+ if ($hour < 0) {
+ $hour += 24;
+ $days--;
+ }
+ $d = $this->day->next($days);
+ $month = $d->getMonth();
+ return self::fromYmdHms($month->getYear()->getYear(), $month->getMonthWithLeap(), $d->getDay(), $hour, $this->minute, $this->second);
+ }
+
+ /**
+ * 当时的年干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getYearSixtyCycle(): SixtyCycle
+ {
+ $solarTime = $this->getSolarTime();
+ $solarYear = $this->day->getSolarDay()->getMonth()->getYear()->getYear();
+ $springSolarTime = SolarTerm::fromIndex($solarYear, 3)->getJulianDay()->getSolarTime();
+ $lunarYear = $this->day->getMonth()->getYear();
+ $year = $lunarYear->getYear();
+ $sixtyCycle = $lunarYear->getSixtyCycle();
+ if ($year == $solarYear) {
+ if ($solarTime->isBefore($springSolarTime)) {
+ $sixtyCycle = $sixtyCycle->next(-1);
+ }
+ } else if ($year < $solarYear) {
+ if (!$solarTime->isBefore($springSolarTime)) {
+ $sixtyCycle = $sixtyCycle->next(1);
+ }
+ }
+ return $sixtyCycle;
+ }
+
+ /**
+ * 当时的月干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getMonthSixtyCycle(): SixtyCycle
+ {
+ $solarTime = $this->getSolarTime();
+ $year = $solarTime->getDay()->getMonth()->getYear()->getYear();
+ $term = $solarTime->getTerm();
+ $index = $term->getIndex() - 3;
+ if ($index < 0 && $term->getJulianDay()->getSolarTime()->isAfter(SolarTerm::fromIndex($year, 3)->getJulianDay()->getSolarTime())) {
+ $index += 24;
+ }
+ return LunarMonth::fromYm($year, 1)->getSixtyCycle()->next((int)floor($index / 2));
+ }
+
+ /**
+ * 当时的日干支(23:00开始算做第二天)
+ *
+ * @return SixtyCycle 干支
+ */
+ function getDaySixtyCycle(): SixtyCycle
+ {
+ $d = $this->day->getSixtyCycle();
+ return $this->hour > 22 ? $d->next(1) : $d;
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ $earthBranchIndex = $this->getIndexInDay() % 12;
+ $heavenStemIndex = $this->getDaySixtyCycle()->getHeavenStem()->getIndex() % 5 * 2 + $earthBranchIndex;
+ return SixtyCycle::fromName(sprintf('%s%s', HeavenStem::fromIndex($heavenStemIndex)->getName(), EarthBranch::fromIndex($earthBranchIndex)->getName()));
+ }
+
+ /**
+ * 九星(时家紫白星歌诀:三元时白最为佳,冬至阳生顺莫差,孟日七宫仲一白,季日四绿发萌芽,每把时辰起甲子,本时星耀照光华,时星移入中宫去,顺飞八方逐细查。夏至阴生逆回首,孟归三碧季加六,仲在九宫时起甲,依然掌中逆轮跨。)
+ *
+ * @return NineStar 九星
+ */
+ function getNineStar(): NineStar
+ {
+ $solar = $this->day->getSolarDay();
+ $dongZhi = SolarTerm::fromIndex($solar->getMonth()->getYear()->getYear(), 0);
+ $xiaZhi = $dongZhi->next(12);
+ $asc = !$solar->isBefore($dongZhi->getJulianDay()->getSolarDay()) && $solar->isBefore($xiaZhi->getJulianDay()->getSolarDay());
+ $start = [8, 5, 2][$this->day->getSixtyCycle()->getEarthBranch()->getIndex() % 3];
+ if ($asc) {
+ $start = 8 - $start;
+ }
+ $earthBranchIndex = $this->getIndexInDay() % 12;
+ return NineStar::fromIndex($start + ($asc ? $earthBranchIndex : -$earthBranchIndex));
+ }
+
+ /**
+ * 公历时刻
+ *
+ * @return SolarTime 公历时刻
+ */
+ function getSolarTime(): SolarTime
+ {
+ $d = $this->day->getSolarDay();
+ $m = $d->getMonth();
+ return SolarTime::fromYmdHms($m->getYear()->getYear(), $m->getMonth(), $d->getDay(), $this->hour, $this->minute, $this->second);
+ }
+
+ /**
+ * 八字
+ *
+ * @return EightChar 八字
+ */
+ function getEightChar(): EightChar
+ {
+ return new EightChar($this->getYearSixtyCycle(), $this->getMonthSixtyCycle(), $this->getDaySixtyCycle(), $this->getSixtyCycle());
+ }
+
+ function equals(mixed $o): bool
+ {
+ if (!($o instanceof LunarHour)) {
+ return false;
+ }
+ return $this->day->equals($o->getDay()) && $this->hour == $o->getHour() && $this->minute == $o->getMinute() && $this->second == $o->getSecond();
+ }
+}
diff --git a/src/lunar/LunarMonth.php b/src/lunar/LunarMonth.php
new file mode 100644
index 0000000..edb300f
--- /dev/null
+++ b/src/lunar/LunarMonth.php
@@ -0,0 +1,341 @@
+getLeapMonth();
+ if ($month == 0 || $month > 12 || $month < -12) {
+ throw new InvalidArgumentException(sprintf('illegal lunar month: %d', $month));
+ }
+ $leap = $month < 0;
+ $m = abs($month);
+ if ($leap && $m != $currentLeapMonth) {
+ throw new InvalidArgumentException(sprintf('illegal leap month %d in lunar year %d', $m, $year));
+ }
+
+ // 冬至
+ $dongZhi = SolarTerm::fromIndex($year, 0);
+ $dongZhiJd = $dongZhi->getCursoryJulianDay();
+
+ // 冬至前的初一,今年首朔的日月黄经差
+ $w = ShouXingUtil::calcShuo($dongZhiJd);
+ if ($w > $dongZhiJd) {
+ $w -= 29.53;
+ }
+
+ // 计算正月初一的偏移
+ $prevYear = LunarYear::fromYear($year - 1);
+ $prevLeapMonth = $prevYear->getLeapMonth();
+
+ // 正常情况正月初一为第3个朔日,但有些特殊的
+ $offset = 2;
+ if ($year > 8 && $year < 24) {
+ $offset = 1;
+ } else if ($prevLeapMonth > 10 && $year != 239 && $year != 240) {
+ $offset = 3;
+ }
+
+ // 位于当年的索引
+ $index = $m - 1;
+ if ($leap || ($currentLeapMonth > 0 && $m > $currentLeapMonth)) {
+ $index += 1;
+ }
+ $this->indexInYear = $index;
+
+ // 本月初一
+ $w += 29.5306 * ($offset + $index);
+ $firstDay = ShouXingUtil::calcShuo($w);
+ $this->firstJulianDay = JulianDay::fromJulianDay(JulianDay::$J2000 + $firstDay);
+ // 本月天数 = 下月初一 - 本月初一
+ $this->dayCount = (int)(ShouXingUtil::calcShuo($w + 29.5306) - $firstDay);
+ $this->year = $currentYear;
+ $this->month = $m;
+ $this->leap = $leap;
+ }
+
+ static function fromYm(int $year, int $month): static
+ {
+ return new static($year, $month);
+ }
+
+ /**
+ * 农历年
+ *
+ * @return LunarYear 农历年
+ */
+ function getYear(): LunarYear
+ {
+ return $this->year;
+ }
+
+ /**
+ * 月
+ *
+ * @return int 月
+ */
+ function getMonth(): int
+ {
+ return $this->month;
+ }
+
+ /**
+ * 月
+ *
+ * @return int 月,当月为闰月时,返回负数
+ */
+ function getMonthWithLeap(): int
+ {
+ return $this->leap ? -$this->month : $this->month;
+ }
+
+ /**
+ * 天数(大月30天,小月29天)
+ *
+ * @return int 天数
+ */
+ function getDayCount(): int
+ {
+ return $this->dayCount;
+ }
+
+ /**
+ * 位于当年的索引(0-12)
+ *
+ * @return int 索引
+ */
+ function getIndexInYear(): int
+ {
+ return $this->indexInYear;
+ }
+
+ /**
+ * 农历季节
+ *
+ * @return LunarSeason 农历季节
+ */
+ function getSeason(): LunarSeason
+ {
+ return LunarSeason::fromIndex($this->month - 1);
+ }
+
+ /**
+ * 初一的儒略日
+ *
+ * @return JulianDay 儒略日
+ */
+ function getFirstJulianDay(): JulianDay
+ {
+ return $this->firstJulianDay;
+ }
+
+ /**
+ * 是否闰月
+ *
+ * @return bool true/false
+ */
+ function isLeap(): bool
+ {
+ return $this->leap;
+ }
+
+ /**
+ * 周数
+ *
+ * @param int $start 起始星期,1234560分别代表星期一至星期天
+ * @return int 周数
+ */
+ function getWeekCount(int $start): int
+ {
+ return (int)ceil(($this->indexOf($this->firstJulianDay->getWeek()->getIndex() - $start, null, 7) + $this->getDayCount()) / 7);
+ }
+
+ /**
+ * 依据国家标准《农历的编算和颁行》GB/T 33661-2017中农历月的命名方法。
+ *
+ * @return string 名称
+ */
+ function getName(): string
+ {
+ return sprintf('%s%s', $this->leap ? '闰' : '', self::$NAMES[$this->month - 1]);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->year, $this->getName());
+ }
+
+ function next(int $n): LunarMonth
+ {
+ if ($n == 0) {
+ return static::fromYm($this->year->getYear(), $this->getMonthWithLeap());
+ }
+ $m = $this->indexInYear + 1 + $n;
+ $y = $this->year;
+ $leapMonth = $y->getLeapMonth();
+ $monthSize = 12 + ($leapMonth > 0 ? 1 : 0);
+ $forward = $n > 0;
+ $add = $forward ? 1 : -1;
+ while ($forward ? ($m > $monthSize) : ($m <= 0)) {
+ if ($forward) {
+ $m -= $monthSize;
+ }
+ $y = $y->next($add);
+ $leapMonth = $y->getLeapMonth();
+ $monthSize = 12 + ($leapMonth > 0 ? 1 : 0);
+ if (!$forward) {
+ $m += $monthSize;
+ }
+ }
+ $leap = false;
+ if ($leapMonth > 0) {
+ if ($m == $leapMonth + 1) {
+ $leap = true;
+ }
+ if ($m > $leapMonth) {
+ $m--;
+ }
+ }
+ return static::fromYm($y->getYear(), $leap ? -$m : $m);
+ }
+
+ /**
+ * 获取本月的农历日列表
+ *
+ * @return LunarDay[] 农历日列表
+ */
+ function getDays(): array
+ {
+ $size = $this->getDayCount();
+ $y = $this->year->getYear();
+ $m = $this->getMonthWithLeap();
+ $l = array();
+ for ($i = 0; $i < $size; $i++) {
+ $l[] = LunarDay::fromYmd($y, $m, $i + 1);
+ }
+ return $l;
+ }
+
+ /**
+ * 获取本月的农历周列表
+ *
+ * @param int $start 星期几作为一周的开始,1234560分别代表星期一至星期天
+ * @return LunarWeek[] 周列表
+ */
+ function getWeeks(int $start): array
+ {
+ $size = $this->getWeekCount($start);
+ $y = $this->year->getYear();
+ $m = $this->getMonthWithLeap();
+ $l = array();
+ for ($i = 0; $i < $size; $i++) {
+ $l[] = LunarWeek::fromYm($y, $m, $i, $start);
+ }
+ return $l;
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ return SixtyCycle::fromName(sprintf('%s%s', HeavenStem::fromIndex(($this->year->getSixtyCycle()->getHeavenStem()->getIndex() + 1) * 2 + $this->indexInYear)->getName(), EarthBranch::fromIndex($this->indexInYear + 2)->getName()));
+ }
+
+ /**
+ * 九星
+ *
+ * @return NineStar 九星
+ */
+ function getNineStar(): NineStar
+ {
+ return NineStar::fromIndex(27 - $this->year->getSixtyCycle()->getEarthBranch()->getIndex() % 3 * 3 - $this->getSixtyCycle()->getEarthBranch()->getIndex());
+ }
+
+ /**
+ * 太岁方位
+ *
+ * @return Direction 方位
+ */
+ function getJupiterDirection(): Direction
+ {
+ $sixtyCycle = $this->getSixtyCycle();
+ $n = [7, -1, 1, 3][$sixtyCycle->getEarthBranch()->next(-2)->getIndex() % 4];
+ return $n == -1 ? $sixtyCycle->getHeavenStem()->getDirection() : Direction::fromIndex($n);
+ }
+
+ /**
+ * 逐月胎神
+ *
+ * @return FetusMonth 逐月胎神
+ */
+ function getFetus(): FetusMonth
+ {
+ return FetusMonth::fromLunarMonth($this);
+ }
+
+ function equals(mixed $o): bool
+ {
+ if (!($o instanceof LunarMonth)) {
+ return false;
+ }
+ return $this->year->equals($o->getYear()) && $this->getMonthWithLeap() == $o->getMonthWithLeap();
+ }
+
+}
diff --git a/src/lunar/LunarSeason.php b/src/lunar/LunarSeason.php
new file mode 100644
index 0000000..b1beb2d
--- /dev/null
+++ b/src/lunar/LunarSeason.php
@@ -0,0 +1,40 @@
+nextIndex($n));
+ }
+}
diff --git a/src/lunar/LunarWeek.php b/src/lunar/LunarWeek.php
new file mode 100644
index 0000000..7d0e20c
--- /dev/null
+++ b/src/lunar/LunarWeek.php
@@ -0,0 +1,158 @@
+ 5) {
+ throw new InvalidArgumentException(sprintf('illegal lunar week index: %d', $index));
+ }
+ if ($start < 0 || $start > 6) {
+ throw new InvalidArgumentException(sprintf('illegal lunar week start: %d', $start));
+ }
+ $m = LunarMonth::fromYm($year, $month);
+ if ($index >= $m->getWeekCount($start)) {
+ throw new InvalidArgumentException(sprintf('illegal lunar week index: %d in month: %s', $index, $m));
+ }
+ $this->month = $m;
+ $this->index = $index;
+ $this->start = Week::fromIndex($start);
+ }
+
+ static function fromYm(int $year, int $month, int $index, int $start): static
+ {
+ return new static($year, $month, $index, $start);
+ }
+
+ /**
+ * 月
+ *
+ * @return LunarMonth 月
+ */
+ function getMonth(): LunarMonth
+ {
+ return $this->month;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引,0-5
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ /**
+ * 起始星期
+ *
+ * @return Week 星期
+ */
+ function getStart(): Week
+ {
+ return $this->start;
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->index];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->month, $this->getName());
+ }
+
+ function next(int $n): static
+ {
+ if ($n == 0) {
+ return static::fromYm($this->month->getYear()->getYear(), $this->month->getMonthWithLeap(), $this->index, $this->start->getIndex());
+ }
+ $d = $this->index + $n;
+ $m = $this->month;
+ $startIndex = $this->start->getIndex();
+ $weeksInMonth = $m->getWeekCount($startIndex);
+ $forward = $n > 0;
+ $add = $forward ? 1 : -1;
+ while ($forward ? ($d >= $weeksInMonth) : ($d < 0)) {
+ if ($forward) {
+ $d -= $weeksInMonth;
+ }
+ if (!$forward) {
+ if (!LunarDay::fromYmd($m->getYear()->getYear(), $m->getMonthWithLeap(), 1)->getWeek()->equals($this->start)) {
+ $d += $add;
+ }
+ }
+ $m = $m->next($add);
+ if ($forward) {
+ if (!LunarDay::fromYmd($m->getYear()->getYear(), $m->getMonthWithLeap(), 1)->getWeek()->equals($this->start)) {
+ $d += $add;
+ }
+ }
+ $weeksInMonth = $m->getWeekCount($startIndex);
+ if (!$forward) {
+ $d += $weeksInMonth;
+ }
+ }
+ return static::fromYm($m->getYear()->getYear(), $m->getMonthWithLeap(), $d, $startIndex);
+ }
+
+ /**
+ * 本周第1天
+ *
+ * @return LunarDay 公历日
+ */
+ function getFirstDay(): LunarDay
+ {
+ $m = $this->getMonth();
+ $firstDay = LunarDay::fromYmd($m->getYear()->getYear(), $m->getMonthWithLeap(), 1);
+ return $firstDay->next($this->index * 7 - $this->indexOf($firstDay->getWeek()->getIndex() - $this->start->getIndex(), null, 7));
+ }
+
+ /**
+ * 本周农历日列表
+ *
+ * @return LunarDay[] 农历日列表
+ */
+ function getDays(): array
+ {
+ $l = array();
+ $d = $this->getFirstDay();
+ $l[] = $d;
+ for ($i = 1; $i < 7; $i++) {
+ $l[] = $d->next($i);
+ }
+ return $l;
+ }
+
+}
diff --git a/src/lunar/LunarYear.php b/src/lunar/LunarYear.php
new file mode 100644
index 0000000..b7cda5d
--- /dev/null
+++ b/src/lunar/LunarYear.php
@@ -0,0 +1,191 @@
+ -1; $x--) {
+ $t += $c * strpos($chars, substr($s, $x, 1));
+ $c *= 64;
+ }
+ $n += $t;
+ $l[] = $n;
+ }
+ $leap[$i + 1] = $l;
+ }
+ self::$LEAP = $leap;
+ }
+
+ protected function __construct(int $year)
+ {
+ if (null == self::$LEAP) {
+ self::init();
+ }
+ if ($year < -1 || $year > 9999) {
+ throw new InvalidArgumentException(sprintf('illegal lunar year: %d', $year));
+ }
+ $this->year = $year;
+ }
+
+ static function fromYear(int $year): static
+ {
+ return new static($year);
+ }
+
+ /**
+ * 年
+ *
+ * @return int 年
+ */
+ function getYear(): int
+ {
+ return $this->year;
+ }
+
+ /**
+ * 天数
+ *
+ * @return int 天数
+ */
+ function getDayCount(): int
+ {
+ $n = 0;
+ foreach ($this->getMonths() as $m) {
+ $n += $m->getDayCount();
+ }
+ return $n;
+ }
+
+ /**
+ * 依据国家标准《农历的编算和颁行》GB/T 33661-2017,农历年有2种命名方法:干支纪年法和生肖纪年法,这里默认采用干支纪年法。
+ *
+ * @return string 名称
+ */
+ function getName(): string
+ {
+ return sprintf('农历%s年', $this->getSixtyCycle());
+ }
+
+ function next(int $n): LunarYear
+ {
+ return $this->fromYear($this->year + $n);
+ }
+
+ /**
+ * 闰月
+ *
+ * @return int 闰月数字,1代表闰1月,0代表无闰月
+ */
+ function getLeapMonth(): int
+ {
+ if ($this->year == -1) {
+ return 11;
+ }
+ foreach (self::$LEAP as $key => $value) {
+ if (in_array($this->year, $value)) {
+ return $key;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * 干支
+ *
+ * @return SixtyCycle 干支
+ */
+ function getSixtyCycle(): SixtyCycle
+ {
+ return SixtyCycle::fromIndex($this->year - 4);
+ }
+
+ /**
+ * 运
+ *
+ * @return Twenty 运
+ */
+ function getTwenty(): Twenty
+ {
+ return Twenty::fromIndex((int)floor(($this->year - 1864) / 20));
+ }
+
+ /**
+ * 九星
+ *
+ * @return NineStar 九星
+ */
+ function getNineStar(): NineStar
+ {
+ return NineStar::fromIndex(63 + $this->getTwenty()->getSixty()->getIndex() * 3 - $this->getSixtyCycle()->getIndex());
+ }
+
+ /**
+ * 太岁方位
+ *
+ * @return Direction 方位
+ */
+ function getJupiterDirection(): Direction
+ {
+ return Direction::fromIndex([0, 7, 7, 2, 3, 3, 8, 1, 1, 6, 0, 0][$this->getSixtyCycle()->getEarthBranch()->getIndex()]);
+ }
+
+ /**
+ * 月份列表
+ *
+ * @return LunarMonth[] 月份列表,一般有12个月,当年有闰月时,有13个月。
+ */
+ function getMonths(): array
+ {
+ $l = array();
+ $m = LunarMonth::fromYm($this->year, 1);
+ while ($m->getYear()->getYear() == $this->year) {
+ $l[] = $m;
+ $m = $m->next(1);
+ }
+ return $l;
+ }
+
+ function equals(mixed $o): bool
+ {
+ return $o instanceof LunarYear && $o->getYear() == $this->year;
+ }
+
+}
diff --git a/src/sixtycycle/EarthBranch.php b/src/sixtycycle/EarthBranch.php
new file mode 100644
index 0000000..58aa714
--- /dev/null
+++ b/src/sixtycycle/EarthBranch.php
@@ -0,0 +1,147 @@
+nextIndex($n));
+ }
+
+ /**
+ * 五行
+ *
+ * @return Element 五行
+ */
+ function getElement(): Element
+ {
+ return Element::fromIndex([4, 2, 0, 0, 2, 1, 1, 2, 3, 3, 2, 4][$this->index]);
+ }
+
+ /**
+ * 阴阳
+ *
+ * @return YinYang 阴阳
+ */
+ function getYinYang(): YinYang
+ {
+ return $this->index % 2 == 0 ? YinYang::YANG : YinYang::YIN;
+ }
+
+ /**
+ * 藏干之本气
+ *
+ * @return HeavenStem 天干
+ */
+ function getHideHeavenStemMain(): HeavenStem
+ {
+ return HeavenStem::fromIndex([9, 5, 0, 1, 4, 2, 3, 5, 6, 7, 4, 8][$this->index]);
+ }
+
+ /**
+ * 藏干之中气,无中气返回null
+ *
+ * @return ?HeavenStem 天干
+ */
+ function getHideHeavenStemMiddle(): ?HeavenStem
+ {
+ $n = [-1, 9, 2, -1, 1, 6, 5, 3, 8, -1, 7, 0][$this->index];
+ return $n == -1 ? null : HeavenStem::fromIndex($n);
+ }
+
+ /**
+ * 藏干之余气,无余气返回null
+ *
+ * @return ?HeavenStem 天干
+ */
+ function getHideHeavenStemResidual(): ?HeavenStem
+ {
+ $n = [-1, 7, 4, -1, 9, 4, -1, 1, 4, -1, 3, -1][$this->index];
+ return $n == -1 ? null : HeavenStem::fromIndex($n);
+ }
+
+ /**
+ * 生肖
+ *
+ * @return Zodiac 生肖
+ */
+ function getZodiac(): Zodiac
+ {
+ return Zodiac::fromIndex($this->index);
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return Direction::fromIndex([0, 4, 2, 2, 4, 8, 8, 4, 6, 6, 4, 0][$this->index]);
+ }
+
+ /**
+ * 相冲的地支(子午冲,丑未冲,寅申冲,辰戌冲,卯酉冲,巳亥冲)
+ *
+ * @return EarthBranch 地支
+ */
+ function getOpposite(): static
+ {
+ return $this->next(6);
+ }
+
+ /**
+ * 煞(逢巳日、酉日、丑日必煞东;亥日、卯日、未日必煞西;申日、子日、辰日必煞南;寅日、午日、戌日必煞北。)
+ *
+ * @return Direction 方位
+ */
+ function getOminous(): Direction
+ {
+ return Direction::fromIndex([8, 2, 0, 6][$this->index % 4]);
+ }
+
+ /**
+ * 地支彭祖百忌
+ *
+ * @return PengZuEarthBranch 地支彭祖百忌
+ */
+ function getPengZuEarthBranch(): PengZuEarthBranch
+ {
+ return PengZuEarthBranch::fromIndex($this->index);
+ }
+}
diff --git a/src/sixtycycle/HeavenStem.php b/src/sixtycycle/HeavenStem.php
new file mode 100644
index 0000000..4784eb8
--- /dev/null
+++ b/src/sixtycycle/HeavenStem.php
@@ -0,0 +1,172 @@
+nextIndex($n));
+ }
+
+ /**
+ * 五行
+ *
+ * @return Element 五行
+ */
+ function getElement(): Element
+ {
+ return Element::fromIndex(intdiv($this->index, 2));
+ }
+
+ /**
+ * 阴阳
+ *
+ * @return YinYang 阴阳
+ */
+ function getYinYang(): YinYang
+ {
+ return $this->index % 2 == 0 ? YinYang::YANG : YinYang::YIN;
+ }
+
+ /**
+ * 十神(生我者,正印偏印。我生者,伤官食神。克我者,正官七杀。我克者,正财偏财。同我者,劫财比肩。)
+ *
+ * @param HeavenStem $target 天干
+ * @return TenStar 十神
+ */
+ function getTenStar(HeavenStem $target): TenStar
+ {
+ $hostElement = $this->getElement();
+ $guestElement = $target->getElement();
+ $index = 0;
+ $sameYinYang = $this->getYinYang()->equals($target->getYinYang());
+ if ($hostElement->getReinforce()->equals($guestElement)) {
+ $index = 1;
+ } else if ($hostElement->getRestrain()->equals($guestElement)) {
+ $index = 2;
+ } else if ($guestElement->getRestrain()->equals($hostElement)) {
+ $index = 3;
+ } else if ($guestElement->getReinforce()->equals($hostElement)) {
+ $index = 4;
+ }
+ return TenStar::fromIndex($index * 2 + ($sameYinYang ? 0 : 1));
+ }
+
+ /**
+ * 方位
+ *
+ * @return Direction 方位
+ */
+ function getDirection(): Direction
+ {
+ return Direction::fromIndex([2, 8, 4, 6, 0][intdiv($this->index, 2)]);
+ }
+
+ /**
+ * 喜神方位(《喜神方位歌》甲己在艮乙庚乾,丙辛坤位喜神安。丁壬只在离宫坐,戊癸原在在巽间。)
+ *
+ * @return Direction 方位
+ */
+ function getJoyDirection(): Direction
+ {
+ return Direction::fromIndex([7, 5, 1, 8, 3][$this->index % 5]);
+ }
+
+ /**
+ * 阳贵神方位(《阳贵神歌》甲戊坤艮位,乙己是坤坎,庚辛居离艮,丙丁兑与乾,震巽属何日,壬癸贵神安。)
+ *
+ * @return Direction 方位
+ */
+ function getYangDirection(): Direction
+ {
+ return Direction::fromIndex([1, 1, 6, 5, 7, 0, 8, 7, 2, 3][$this->index]);
+ }
+
+ /**
+ * 阴贵神方位(《阴贵神歌》甲戊见牛羊,乙己鼠猴乡,丙丁猪鸡位,壬癸蛇兔藏,庚辛逢虎马,此是贵神方。)
+ *
+ * @return Direction 方位
+ */
+ function getYinDirection(): Direction
+ {
+ return Direction::fromIndex([7, 0, 5, 6, 1, 1, 7, 8, 3, 2][$this->index]);
+ }
+
+ /**
+ * 财神方位(《财神方位歌》甲乙东北是财神,丙丁向在西南寻,戊己正北坐方位,庚辛正东去安身,壬癸原来正南坐,便是财神方位真。)
+ *
+ * @return Direction 方位
+ */
+ function getWealthDirection(): Direction
+ {
+ return Direction::fromIndex([7, 1, 0, 2, 8][intdiv($this->index, 2)]);
+ }
+
+ /**
+ * 福神方位(《福神方位歌》甲乙东南是福神,丙丁正东是堪宜,戊北己南庚辛坤,壬在乾方癸在西。)
+ *
+ * @return Direction 方位
+ */
+ function getMascotDirection(): Direction
+ {
+ return Direction::fromIndex([3, 3, 2, 2, 0, 8, 1, 1, 5, 6][$this->index]);
+ }
+
+ /**
+ * 天干彭祖百忌
+ *
+ * @return PengZuHeavenStem 天干彭祖百忌
+ */
+ function getPengZuHeavenStem(): PengZuHeavenStem
+ {
+ return PengZuHeavenStem::fromIndex($this->index);
+ }
+
+ /**
+ * 地势(长生十二神)
+ *
+ * @param EarthBranch $earthBranch 地支
+ * @return Terrain 地势(长生十二神)
+ */
+ function getTerrain(EarthBranch $earthBranch): Terrain
+ {
+ $earthBranchIndex = $earthBranch->getIndex();
+ return Terrain::fromIndex([1, 6, 10, 9, 10, 9, 7, 0, 4, 3][$this->index] + (YinYang::YANG == $this->getYinYang() ? $earthBranchIndex : -$earthBranchIndex));
+ }
+}
diff --git a/src/sixtycycle/SixtyCycle.php b/src/sixtycycle/SixtyCycle.php
new file mode 100644
index 0000000..24ac1bf
--- /dev/null
+++ b/src/sixtycycle/SixtyCycle.php
@@ -0,0 +1,107 @@
+nextIndex($n));
+ }
+
+ /**
+ * 天干
+ *
+ * @return HeavenStem 天干
+ */
+ function getHeavenStem(): HeavenStem
+ {
+ return HeavenStem::fromIndex($this->index % count(HeavenStem::$NAMES));
+ }
+
+ /**
+ * 地支
+ *
+ * @return EarthBranch 地支
+ */
+ function getEarthBranch(): EarthBranch
+ {
+ return EarthBranch::fromIndex($this->index % count(EarthBranch::$NAMES));
+ }
+
+ /**
+ * 纳音
+ *
+ * @return Sound 纳音
+ */
+ function getSound(): Sound
+ {
+ return Sound::fromIndex(intdiv($this->index, 2));
+ }
+
+ /**
+ * 彭祖百忌
+ *
+ * @return PengZu 彭祖百忌
+ */
+ function getPengZu(): PengZu
+ {
+ return PengZu::fromSixtyCycle($this);
+ }
+
+ /**
+ * 旬
+ *
+ * @return Ten 旬
+ */
+ function getTen(): Ten
+ {
+ return Ten::fromIndex(intdiv($this->getHeavenStem()->getIndex() - $this->getEarthBranch()->getIndex(), 2));
+ }
+
+ /**
+ * 旬空(空亡),因地支比天干多2个,旬空则为每一轮干支一一配对后多出来的2个地支
+ *
+ * @return EarthBranch[] 旬空(空亡)
+ */
+ function getExtraEarthBranches(): array
+ {
+ $l = array();
+ $l[] = EarthBranch::fromIndex(10 + $this->getEarthBranch()->getIndex() - $this->getHeavenStem()->getIndex());
+ $l[] = $l[0]->next(1);
+ return $l;
+ }
+
+}
diff --git a/src/solar/SolarDay.php b/src/solar/SolarDay.php
new file mode 100644
index 0000000..be6c564
--- /dev/null
+++ b/src/solar/SolarDay.php
@@ -0,0 +1,362 @@
+month = SolarMonth::fromYm($year, $month);
+ if ($day < 1) {
+ throw new InvalidArgumentException(sprintf('illegal solar day: %d-%d-%d', $year, $month, $day));
+ }
+ if (1582 == $year && 10 == $month) {
+ if ($day > 4 && $day < 15) {
+ throw new InvalidArgumentException(sprintf('illegal solar day: %d-%d-%d', $year, $month, $day));
+ } else if ($day > 31) {
+ throw new InvalidArgumentException(sprintf('illegal solar day: %d-%d-%d', $year, $month, $day));
+ }
+ } else if ($day > SolarMonth::fromYm($year, $month)->getDayCount()) {
+ throw new InvalidArgumentException(sprintf('illegal solar day: %d-%d-%d', $year, $month, $day));
+ }
+ $this->day = $day;
+ }
+
+ static function fromYmd(int $year, int $month, int $day): static
+ {
+ return new static($year, $month, $day);
+ }
+
+ /**
+ * 月
+ *
+ * @return SolarMonth 月
+ */
+ function getMonth(): SolarMonth
+ {
+ return $this->month;
+ }
+
+ /**
+ * 日
+ *
+ * @return int 日
+ */
+ function getDay(): int
+ {
+ return $this->day;
+ }
+
+ /**
+ * 星期
+ *
+ * @return Week 星期
+ */
+ function getWeek(): Week
+ {
+ return $this->getJulianDay()->getWeek();
+ }
+
+ /**
+ * 星座
+ *
+ * @return Constellation 星座
+ */
+ function getConstellation(): Constellation
+ {
+ $index = 11;
+ $y = $this->month->getMonth() * 100 + $this->day;
+ if ($y >= 321 && $y <= 419) {
+ $index = 0;
+ } else if ($y >= 420 && $y <= 520) {
+ $index = 1;
+ } else if ($y >= 521 && $y <= 621) {
+ $index = 2;
+ } else if ($y >= 622 && $y <= 722) {
+ $index = 3;
+ } else if ($y >= 723 && $y <= 822) {
+ $index = 4;
+ } else if ($y >= 823 && $y <= 922) {
+ $index = 5;
+ } else if ($y >= 923 && $y <= 1023) {
+ $index = 6;
+ } else if ($y >= 1024 && $y <= 1122) {
+ $index = 7;
+ } else if ($y >= 1123 && $y <= 1221) {
+ $index = 8;
+ } else if ($y >= 1222 || $y <= 119) {
+ $index = 9;
+ } else if ($y <= 218) {
+ $index = 10;
+ }
+ return Constellation::fromIndex($index);
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->day - 1];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->month, $this->getName());
+ }
+
+ function next(int $n): SolarDay
+ {
+ return $this->getJulianDay()->next($n)->getSolarDay();
+ }
+
+ /**
+ * 是否在指定公历日之前
+ *
+ * @param SolarDay $target 公历日
+ * @return bool true/false
+ */
+ function isBefore(SolarDay $target): bool
+ {
+ $aYear = $this->month->getYear()->getYear();
+ $targetMonth = $target->getMonth();
+ $bYear = $targetMonth->getYear()->getYear();
+ if ($aYear == $bYear) {
+ $aMonth = $this->month->getMonth();
+ $bMonth = $targetMonth->getMonth();
+ return $aMonth == $bMonth ? $this->day < $target->getDay() : $aMonth < $bMonth;
+ }
+ return $aYear < $bYear;
+ }
+
+ /**
+ * 是否在指定公历日之后
+ *
+ * @param SolarDay $target 公历日
+ * @return bool true/false
+ */
+ function isAfter(SolarDay $target): bool
+ {
+ $aYear = $this->month->getYear()->getYear();
+ $targetMonth = $target->getMonth();
+ $bYear = $targetMonth->getYear()->getYear();
+ if ($aYear == $bYear) {
+ $aMonth = $this->month->getMonth();
+ $bMonth = $targetMonth->getMonth();
+ return $aMonth == $bMonth ? $this->day > $target->getDay() : $aMonth > $bMonth;
+ }
+ return $aYear > $bYear;
+ }
+
+ /**
+ * 节气
+ *
+ * @return SolarTerm 节气
+ */
+ function getTerm(): SolarTerm
+ {
+ $term = SolarTerm::fromIndex($this->month->getYear()->getYear() + 1, 0);
+ while ($this->isBefore($term->getJulianDay()->getSolarDay())) {
+ $term = $term->next(-1);
+ }
+ return $term;
+ }
+
+ /**
+ * 七十二候
+ *
+ * @return PhenologyDay 七十二候
+ */
+ function getPhenologyDay(): PhenologyDay
+ {
+ $term = $this->getTerm();
+ $dayIndex = $this->subtract($term->getJulianDay()->getSolarDay());
+ $index = intdiv($dayIndex, 5);
+ if ($index > 2) {
+ $index = 2;
+ }
+ $dayIndex -= $index * 5;
+ return new PhenologyDay(Phenology::fromIndex($term->getIndex() * 3 + $index), $dayIndex);
+ }
+
+ /**
+ * 三伏天
+ *
+ * @return DogDay|null 三伏天
+ */
+ function getDogDay(): ?DogDay
+ {
+ $xiaZhi = SolarTerm::fromIndex($this->month->getYear()->getYear(), 12);
+ // 第1个庚日
+ $start = $xiaZhi->getJulianDay()->getSolarDay();
+ $add = 6 - $start->getLunarDay()->getSixtyCycle()->getHeavenStem()->getIndex();
+ if ($add < 0) {
+ $add += 10;
+ }
+ // 第3个庚日,即初伏第1天
+ $add += 20;
+ $start = $start->next($add);
+ $days = $this->subtract($start);
+ // 初伏以前
+ if ($days < 0) {
+ return null;
+ }
+ if ($days < 10) {
+ return new DogDay(Dog::fromIndex(0), $days);
+ }
+ // 第4个庚日,中伏第1天
+ $start = $start->next(10);
+ $days = $this->subtract($start);
+ if ($days < 10) {
+ return new DogDay(Dog::fromIndex(1), $days);
+ }
+ // 第5个庚日,中伏第11天或末伏第1天
+ $start = $start->next(10);
+ $days = $this->subtract($start);
+ // 立秋
+ if ($xiaZhi->next(3)->getJulianDay()->getSolarDay()->isAfter($start)) {
+ if ($days < 10) {
+ return new DogDay(Dog::fromIndex(1), $days + 10);
+ }
+ $start = $start->next(10);
+ $days = $this->subtract($start);
+ }
+ if ($days < 10) {
+ return new DogDay(Dog::fromIndex(2), $days);
+ }
+ return null;
+ }
+
+ /**
+ * 数九天
+ *
+ * @return NineDay|null 数九天
+ */
+ function getNineDay(): ?NineDay
+ {
+ $year = $this->month->getYear()->getYear();
+ $start = SolarTerm::fromIndex($year + 1, 0)->getJulianDay()->getSolarDay();
+ if ($this->isBefore($start)) {
+ $start = SolarTerm::fromIndex($year, 0)->getJulianDay()->getSolarDay();
+ }
+ $end = $start->next(81);
+ if ($this->isBefore($start) || !$this->isBefore($end)) {
+ return null;
+ }
+ $days = $this->subtract($start);
+ return new NineDay(Nine::fromIndex(intdiv($days, 9)), $days % 9);
+ }
+
+ /**
+ * 位于当年的索引
+ *
+ * @return int 索引
+ */
+ function getIndexInYear(): int
+ {
+ $m = $this->month->getMonth();
+ $y = $this->month->getYear()->getYear();
+ $days = 0;
+ for ($i = 1; $i < $m; $i++) {
+ $days += SolarMonth::fromYm($y, $i)->getDayCount();
+ }
+ $d = $this->day;
+ if (1582 == $y && 10 == $m) {
+ if ($d >= 15) {
+ $d -= 10;
+ }
+ }
+ return $days + $d - 1;
+ }
+
+ /**
+ * 公历日期相减,获得相差天数
+ *
+ * @param SolarDay $target 公历
+ * @return int 天数
+ */
+ function subtract(SolarDay $target): int
+ {
+ return (int)($this->getJulianDay()->getDay() - $target->getJulianDay()->getDay());
+ }
+
+ /**
+ * 儒略日
+ *
+ * @return JulianDay 儒略日
+ */
+ function getJulianDay(): JulianDay
+ {
+ return JulianDay::fromYmdHms($this->month->getYear()->getYear(), $this->month->getMonth(), $this->day, 0, 0, 0);
+ }
+
+ /**
+ * 农历日
+ *
+ * @return LunarDay 农历日
+ */
+ function getLunarDay(): LunarDay
+ {
+ $m = LunarMonth::fromYm($this->month->getYear()->getYear(), $this->month->getMonth())->next(-3);
+ $days = $this->subtract($m->getFirstJulianDay()->getSolarDay());
+ while ($days >= $m->getDayCount()) {
+ $m = $m->next(1);
+ $days = $this->subtract($m->getFirstJulianDay()->getSolarDay());
+ }
+ return LunarDay::fromYmd($m->getYear()->getYear(), $m->getMonthWithLeap(), $days + 1);
+ }
+
+ /**
+ * 法定假日,如果当天不是法定假日,返回null
+ *
+ * @return ?LegalHoliday 法定假日
+ */
+ function getLegalHoliday(): ?LegalHoliday
+ {
+ $m = $this->getMonth();
+ return LegalHoliday::fromYmd($m->getYear()->getYear(), $m->getMonth(), $this->day);
+ }
+
+ /**
+ * 公历现代节日,如果当天不是公历现代节日,返回null
+ *
+ * @return ?SolarFestival 公历现代节日
+ */
+ function getFestival(): ?SolarFestival
+ {
+ $m = $this->getMonth();
+ return SolarFestival::fromYmd($m->getYear()->getYear(), $m->getMonth(), $this->day);
+ }
+
+}
diff --git a/src/solar/SolarHalfYear.php b/src/solar/SolarHalfYear.php
new file mode 100644
index 0000000..fff33b2
--- /dev/null
+++ b/src/solar/SolarHalfYear.php
@@ -0,0 +1,107 @@
+year = SolarYear::fromYear($year);
+ if ($index < 0 || $index > 1) {
+ throw new InvalidArgumentException(sprintf('illegal solar half year index: %d', $index));
+ }
+ $this->index = $index;
+ }
+
+ static function fromIndex(int $year, int $index): static
+ {
+ return new static($year, $index);
+ }
+
+ /**
+ * 年
+ * @return SolarYear 年
+ */
+ function getYear(): SolarYear
+ {
+ return $this->year;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引,0-1
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->index];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->year, $this->getName());
+ }
+
+ function next(int $n): static
+ {
+ $m = $this->index + $n;
+ return self::fromIndex($this->year->getYear() + intdiv($m, 2), abs($m % 2));
+ }
+
+ /**
+ * 月份列表
+ *
+ * @return SolarMonth[] 月份列表,1年有12个月。
+ */
+ function getMonths(): array
+ {
+ $l = array();
+ $y = $this->year->getYear();
+ for ($i = 0; $i < 6; $i++) {
+ $l[] = SolarMonth::fromYm($y, $this->index * 6 + $i + 1);
+ }
+ return $l;
+ }
+
+ /**
+ * 季度列表
+ *
+ * @return SolarSeason[] 季度列表,1年有4个季度。
+ */
+ function getSeasons(): array
+ {
+ $l = array();
+ $y = $this->year->getYear();
+ for ($i = 0; $i < 2; $i++) {
+ $l[] = SolarSeason::fromIndex($y, $this->index * 2 + $i);
+ }
+ return $l;
+ }
+
+}
diff --git a/src/solar/SolarMonth.php b/src/solar/SolarMonth.php
new file mode 100644
index 0000000..dc50286
--- /dev/null
+++ b/src/solar/SolarMonth.php
@@ -0,0 +1,173 @@
+year = SolarYear::fromYear($year);
+ if ($month < 1 || $month > 12) {
+ throw new InvalidArgumentException(sprintf('illegal solar month: %d', $month));
+ }
+ $this->month = $month;
+ }
+
+ static function fromYm(int $year, int $month): static
+ {
+ return new static($year, $month);
+ }
+
+ /**
+ * 年
+ * @return SolarYear 年
+ */
+ function getYear(): SolarYear
+ {
+ return $this->year;
+ }
+
+ /**
+ * 月
+ *
+ * @return int 月
+ */
+ function getMonth(): int
+ {
+ return $this->month;
+ }
+
+ /**
+ * 天数(1582年10月只有21天)
+ *
+ * @return int 天数
+ */
+ function getDayCount(): int
+ {
+ if (1582 == $this->year->getYear() && 10 == $this->month) {
+ return 21;
+ }
+ $d = self::$DAYS[$this->getIndexInYear()];
+ //公历闰年2月多一天
+ if (2 == $this->month && $this->year->isLeap()) {
+ $d++;
+ }
+ return $d;
+ }
+
+ /**
+ * 位于当年的索引(0-11)
+ *
+ * @return int 索引
+ */
+ function getIndexInYear(): int
+ {
+ return $this->month - 1;
+ }
+
+ /**
+ * 公历季度
+ *
+ * @return SolarSeason 公历季度
+ */
+ function getSeason(): SolarSeason
+ {
+ return SolarSeason::fromIndex($this->year->getYear(), intdiv($this->getIndexInYear(), 3));
+ }
+
+ /**
+ * 周数
+ *
+ * @param int $start 起始星期,1234560分别代表星期一至星期天
+ * @return int 周数
+ */
+ function getWeekCount(int $start): int
+ {
+ return (int)ceil(($this->indexOf(SolarDay::fromYmd($this->year->getYear(), $this->month, 1)->getWeek()->getIndex() - $start, null, 7) + $this->getDayCount()) / 7);
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->getIndexInYear()];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->year, $this->getName());
+ }
+
+ function next(int $n): SolarMonth
+ {
+ if ($n == 0) {
+ return self::fromYm($this->year->getYear(), $this->month);
+ }
+ $m = $this->month + $n;
+ $y = $this->year->getYear() + intdiv($m, 12);
+ $m %= 12;
+ if ($m < 1) {
+ $m += 12;
+ $y--;
+ }
+ return self::fromYm($y, $m);
+ }
+
+ /**
+ * 获取本月的公历周列表
+ *
+ * @param int $start 星期几作为一周的开始,1234560分别代表星期一至星期天
+ * @return SolarWeek[] 周列表
+ */
+ function getWeeks(int $start): array
+ {
+ $size = $this->getWeekCount($start);
+ $y = $this->year->getYear();
+ $l = array();
+ for ($i = 0; $i < $size; $i++) {
+ $l[] = SolarWeek::fromYm($y, $this->month, $i, $start);
+ }
+ return $l;
+ }
+
+ /**
+ * 获取本月的公历日列表
+ *
+ * @return SolarDay[] 公历日列表
+ */
+ function getDays(): array
+ {
+ $size = $this->getDayCount();
+ $y = $this->year->getYear();
+ $l = array();
+ for ($i = 0; $i < $size; $i++) {
+ $l[] = SolarDay::fromYmd($y, $this->month, $i + 1);
+ }
+ return $l;
+ }
+}
diff --git a/src/solar/SolarSeason.php b/src/solar/SolarSeason.php
new file mode 100644
index 0000000..7f0b2ca
--- /dev/null
+++ b/src/solar/SolarSeason.php
@@ -0,0 +1,87 @@
+year = SolarYear::fromYear($year);
+ if ($index < 0 || $index > 1) {
+ throw new InvalidArgumentException(sprintf('illegal solar half year index: %d', $index));
+ }
+ $this->index = $index;
+ }
+
+ static function fromIndex(int $year, int $index): static
+ {
+ return new static($year, $index);
+ }
+
+ /**
+ * 年
+ * @return SolarYear 年
+ */
+ function getYear(): SolarYear
+ {
+ return $this->year;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引,0-1
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->index];
+ }
+
+ function next(int $n): static
+ {
+ $m = $this->index + $n;
+ return self::fromIndex($this->year->getYear() + intdiv($m, 4), abs($m % 4));
+ }
+
+ /**
+ * 月份列表
+ *
+ * @return SolarMonth[] 月份列表,1年有12个月。
+ */
+ function getMonths(): array
+ {
+ $l = array();
+ $y = $this->year->getYear();
+ for ($i = 0; $i < 3; $i++) {
+ $l[] = SolarMonth::fromYm($y, $this->index * 3 + $i + 1);
+ }
+ return $l;
+ }
+
+}
diff --git a/src/solar/SolarTerm.php b/src/solar/SolarTerm.php
new file mode 100644
index 0000000..deb1b42
--- /dev/null
+++ b/src/solar/SolarTerm.php
@@ -0,0 +1,106 @@
+index;
+ }
+ if ($year !== null) {
+ $this->initByYear($year, $idx);
+ } else if ($cursoryJulianDay !== null) {
+ $this->cursoryJulianDay = $cursoryJulianDay;
+ }
+ }
+
+ protected function initByYear(int $year, int $offset): void
+ {
+ $jd = floor(($year - 2000) * 365.2422 + 180);
+ // 355是2000.12冬至,得到较靠近jd的冬至估计值
+ $w = floor(($jd - 355 + 183) / 365.2422) * 365.2422 + 355;
+ if (ShouXingUtil::calcQi($w) > $jd) {
+ $w -= 365.2422;
+ }
+ $this->cursoryJulianDay = ShouXingUtil::calcQi($w + 15.2184 * $offset);
+ }
+
+ static function fromIndex(int $year, int $index): static
+ {
+ return new static($year, $index);
+ }
+
+ static function fromName(int $year, string $name): static
+ {
+ return new static($year, null, $name);
+ }
+
+ function next(int $n): SolarTerm
+ {
+ return new static(null, $this->nextIndex($n), null, $this->cursoryJulianDay + 15.2184 * $n);
+ }
+
+ /**
+ * 是否节
+ *
+ * @return bool true/false
+ */
+ function isJie(): bool
+ {
+ return $this->index % 2 == 1;
+ }
+
+ /**
+ * 是否气
+ *
+ * @return bool true/false
+ */
+ function isQi(): bool
+ {
+ return $this->index % 2 == 0;
+ }
+
+ /**
+ * 儒略日
+ *
+ * @return JulianDay 儒略日
+ */
+ function getJulianDay(): JulianDay
+ {
+ return JulianDay::fromJulianDay(ShouXingUtil::qiAccurate2($this->cursoryJulianDay) + JulianDay::$J2000);
+ }
+
+ /**
+ * 粗略的儒略日
+ *
+ * @return float 儒略日数
+ */
+ function getCursoryJulianDay(): float
+ {
+ return $this->cursoryJulianDay;
+ }
+
+}
diff --git a/src/solar/SolarTime.php b/src/solar/SolarTime.php
new file mode 100644
index 0000000..37fee14
--- /dev/null
+++ b/src/solar/SolarTime.php
@@ -0,0 +1,221 @@
+ 23) {
+ throw new InvalidArgumentException(sprintf('illegal hour: %d', $hour));
+ }
+ if ($minute < 0 || $minute > 59) {
+ throw new InvalidArgumentException(sprintf('illegal minute: %d', $minute));
+ }
+ if ($second < 0 || $second > 59) {
+ throw new InvalidArgumentException(sprintf('illegal second: %d', $second));
+ }
+ $this->day = SolarDay::fromYmd($year, $month, $day);
+ $this->hour = $hour;
+ $this->minute = $minute;
+ $this->second = $second;
+ }
+
+ static function fromYmdHms(int $year, int $month, int $day, int $hour, int $minute, int $second): static
+ {
+ return new static($year, $month, $day, $hour, $minute, $second);
+ }
+
+ /**
+ * 日
+ *
+ * @return SolarDay 日
+ */
+ function getDay(): SolarDay
+ {
+ return $this->day;
+ }
+
+ /**
+ * 时
+ *
+ * @return int 时
+ */
+ function getHour(): int
+ {
+ return $this->hour;
+ }
+
+ /**
+ * 分
+ *
+ * @return int 分
+ */
+ function getMinute(): int
+ {
+ return $this->minute;
+ }
+
+ /**
+ * 秒
+ *
+ * @return int 秒
+ */
+ function getSecond(): int
+ {
+ return $this->second;
+ }
+
+ function getName(): string
+ {
+ return sprintf('%02d:%02d:%02d', $this->hour, $this->minute, $this->second);
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s %s', $this->day, $this->getName());
+ }
+
+ /**
+ * 是否在指定公历时刻之前
+ *
+ * @param SolarTime $target 公历时刻
+ * @return bool true/false
+ */
+ function isBefore(SolarTime $target): bool
+ {
+ if (!$this->day->equals($target->getDay())) {
+ return $this->day->isBefore($target->getDay());
+ }
+ $bHour = $target->getHour();
+ if ($this->hour == $bHour) {
+ $bMinute = $target->getMinute();
+ return $this->minute == $bMinute ? $this->second < $target->getSecond() : $this->minute < $bMinute;
+ }
+ return $this->hour < $bHour;
+ }
+
+ /**
+ * 是否在指定公历时刻之后
+ *
+ * @param SolarTime $target 公历时刻
+ * @return true/false
+ */
+ function isAfter(SolarTime $target): bool
+ {
+ if (!$this->day->equals($target->getDay())) {
+ return $this->day->isAfter($target->getDay());
+ }
+ $bHour = $target->getHour();
+ if ($this->hour == $bHour) {
+ $bMinute = $target->getMinute();
+ return $this->minute == $bMinute ? $this->second > $target->getSecond() : $this->minute > $bMinute;
+ }
+ return $this->hour > $bHour;
+ }
+
+ /**
+ * 节气
+ *
+ * @return SolarTerm 节气
+ */
+ function getTerm(): SolarTerm
+ {
+ $term = SolarTerm::fromIndex($this->day->getMonth()->getYear()->getYear() + 1, 0);
+ while ($this->isBefore($term->getJulianDay()->getSolarTime())) {
+ $term = $term->next(-1);
+ }
+ return $term;
+ }
+
+ /**
+ * 儒略日
+ *
+ * @return JulianDay 儒略日
+ */
+ function getJulianDay(): JulianDay
+ {
+ $month = $this->day->getMonth();
+ return JulianDay::fromYmdHms($month->getYear()->getYear(), $month->getMonth(), $this->day->getDay(), $this->hour, $this->minute, $this->second);
+ }
+
+ /**
+ * 公历时刻相减,获得相差秒数
+ *
+ * @param SolarTime $target 公历时刻
+ * @return int 秒数
+ */
+ function subtract(SolarTime $target): int
+ {
+ $days = $this->day->subtract($target->getDay());
+ $cs = $this->hour * 3600 + $this->minute * 60 + $this->second;
+ $ts = $target->getHour() * 3600 + $target->getMinute() * 60 + $target->getSecond();
+ $seconds = $cs - $ts;
+ if ($seconds < 0) {
+ $seconds += 86400;
+ $days--;
+ }
+ $seconds += $days * 86400;
+ return $seconds;
+ }
+
+ /**
+ * 推移
+ *
+ * @param int $n 推移秒数
+ * @return SolarTime 公历时刻
+ */
+ function next(int $n): SolarTime
+ {
+ $ts = $this->second + $n;
+ $tm = $this->minute + intdiv($ts, 60);
+ $th = $this->hour + intdiv($tm, 60);
+ $d = $this->day->next(intdiv($th, 24));
+ $month = $d->getMonth();
+ return SolarTime::fromYmdHms($month->getYear()->getYear(), $month->getMonth(), $d->getDay(), $th % 24, $tm % 60, $ts % 60);
+ }
+
+ /**
+ * 时辰
+ *
+ * @return LunarHour 农历时辰
+ */
+ function getLunarHour(): LunarHour
+ {
+ $d = $this->day->getLunarDay();
+ $m = $d->getMonth();
+ return LunarHour::fromYmdHms($m->getYear()->getYear(), $m->getMonthWithLeap(), $d->getDay(), $this->hour, $this->minute, $this->second);
+ }
+
+}
diff --git a/src/solar/SolarWeek.php b/src/solar/SolarWeek.php
new file mode 100644
index 0000000..cac758f
--- /dev/null
+++ b/src/solar/SolarWeek.php
@@ -0,0 +1,158 @@
+ 5) {
+ throw new InvalidArgumentException(sprintf('illegal solar week index: %d', $index));
+ }
+ if ($start < 0 || $start > 6) {
+ throw new InvalidArgumentException(sprintf('illegal solar week start: %d', $start));
+ }
+ $m = SolarMonth::fromYm($year, $month);
+ if ($index >= $m->getWeekCount($start)) {
+ throw new InvalidArgumentException(sprintf('illegal solar week index: %d in month: %s', $index, $m));
+ }
+ $this->month = $m;
+ $this->index = $index;
+ $this->start = Week::fromIndex($start);
+ }
+
+ static function fromYm(int $year, int $month, int $index, int $start): static
+ {
+ return new static($year, $month, $index, $start);
+ }
+
+ /**
+ * 月
+ *
+ * @return SolarMonth 月
+ */
+ function getMonth(): SolarMonth
+ {
+ return $this->month;
+ }
+
+ /**
+ * 索引
+ *
+ * @return int 索引,0-5
+ */
+ function getIndex(): int
+ {
+ return $this->index;
+ }
+
+ /**
+ * 起始星期
+ *
+ * @return Week 星期
+ */
+ function getStart(): Week
+ {
+ return $this->start;
+ }
+
+ function getName(): string
+ {
+ return self::$NAMES[$this->index];
+ }
+
+ function __toString(): string
+ {
+ return sprintf('%s%s', $this->month, $this->getName());
+ }
+
+ function next(int $n): static
+ {
+ if ($n == 0) {
+ return static::fromYm($this->month->getYear()->getYear(), $this->month->getMonth(), $this->index, $this->start->getIndex());
+ }
+ $d = $this->index + $n;
+ $m = $this->month;
+ $startIndex = $this->start->getIndex();
+ $weeksInMonth = $m->getWeekCount($startIndex);
+ $forward = $n > 0;
+ $add = $forward ? 1 : -1;
+ while ($forward ? ($d >= $weeksInMonth) : ($d < 0)) {
+ if ($forward) {
+ $d -= $weeksInMonth;
+ }
+ if (!$forward) {
+ if (!SolarDay::fromYmd($m->getYear()->getYear(), $m->getMonth(), 1)->getWeek()->equals($this->start)) {
+ $d += $add;
+ }
+ }
+ $m = $m->next($add);
+ if ($forward) {
+ if (!SolarDay::fromYmd($m->getYear()->getYear(), $m->getMonth(), 1)->getWeek()->equals($this->start)) {
+ $d += $add;
+ }
+ }
+ $weeksInMonth = $m->getWeekCount($startIndex);
+ if (!$forward) {
+ $d += $weeksInMonth;
+ }
+ }
+ return static::fromYm($m->getYear()->getYear(), $m->getMonth(), $d, $startIndex);
+ }
+
+ /**
+ * 本周第1天
+ *
+ * @return SolarDay 公历日
+ */
+ function getFirstDay(): SolarDay
+ {
+ $m = $this->getMonth();
+ $firstDay = SolarDay::fromYmd($m->getYear()->getYear(), $m->getMonth(), 1);
+ return $firstDay->next($this->index * 7 - $this->indexOf($firstDay->getWeek()->getIndex() - $this->start->getIndex(), null, 7));
+ }
+
+ /**
+ * 本周公历日列表
+ *
+ * @return SolarDay[] 公历日列表
+ */
+ function getDays(): array
+ {
+ $l = array();
+ $d = $this->getFirstDay();
+ $l[] = $d;
+ for ($i = 1; $i < 7; $i++) {
+ $l[] = $d->next($i);
+ }
+ return $l;
+ }
+
+}
diff --git a/src/solar/SolarYear.php b/src/solar/SolarYear.php
new file mode 100644
index 0000000..e2acdab
--- /dev/null
+++ b/src/solar/SolarYear.php
@@ -0,0 +1,121 @@
+ 9999) {
+ throw new InvalidArgumentException(sprintf('illegal solar year: %d', $year));
+ }
+ $this->year = $year;
+ }
+
+ static function fromYear(int $year): static
+ {
+ return new static($year);
+ }
+
+ /**
+ * 年
+ * @return int 年
+ */
+ function getYear(): int
+ {
+ return $this->year;
+ }
+
+ /**
+ * 天数(1582年355天,平年365天,闰年366天)
+ *
+ * @return int 天数
+ */
+ function getDayCount(): int
+ {
+ if (1582 == $this->year) {
+ return 355;
+ }
+ return $this->isLeap() ? 366 : 365;
+ }
+
+ /**
+ * 是否闰年(1582年以前,使用儒略历,能被4整除即为闰年。以后采用格里历,四年一闰,百年不闰,四百年再闰。)
+ *
+ * @return bool true/false
+ */
+ function isLeap(): bool
+ {
+ if ($this->year < 1600) {
+ return $this->year % 4 == 0;
+ }
+ return ($this->year % 4 == 0 && $this->year % 100 != 0) || ($this->year % 400 == 0);
+ }
+
+ function getName(): string
+ {
+ return sprintf('%d年', $this->year);
+ }
+
+ function next(int $n): static
+ {
+ return static::fromYear($this->year + $n);
+ }
+
+ /**
+ * 月份列表
+ *
+ * @return SolarMonth[] 月份列表,1年有12个月。
+ */
+ function getMonths(): array
+ {
+ $l = array();
+ for ($i = 0; $i < 12; $i++) {
+ $l[] = SolarMonth::fromYm($this->year, $i + 1);
+ }
+ return $l;
+ }
+
+ /**
+ * 季度列表
+ *
+ * @return SolarSeason[] 季度列表,1年有4个季度。
+ */
+ function getSeasons(): array
+ {
+ $l = array();
+ for ($i = 0; $i < 4; $i++) {
+ $l[] = SolarSeason::fromIndex($this->year, $i);
+ }
+ return $l;
+ }
+
+ /**
+ * 半年列表
+ *
+ * @return SolarHalfYear[] 半年列表,1年有2个半年。
+ */
+ function getHalfYears(): array
+ {
+ $l = array();
+ for ($i = 0; $i < 2; $i++) {
+ $l[] = SolarHalfYear::fromIndex($this->year, $i);
+ }
+ return $l;
+ }
+
+}
diff --git a/src/util/ShouXingUtil.php b/src/util/ShouXingUtil.php
new file mode 100644
index 0000000..925a482
--- /dev/null
+++ b/src/util/ShouXingUtil.php
@@ -0,0 +1,678 @@
+ $n2) {
+ $m = $n2;
+ }
+ }
+ $c = 0;
+ for ($j = $n1; $j < $m; $j += 3) {
+ $c += self::$XL0[$j] * cos(self::$XL0[$j + 1] + $t * self::$XL0[$j + 2]);
+ }
+ $v += $c * $tn;
+ }
+ $v /= self::$XL0[0];
+ $t2 = $t * $t;
+ $v += (-0.0728 - 2.7702 * $t - 1.1019 * $t2 - 0.0996 * $t2 * $t) / self::$SECOND_PER_RAD;
+ return $v;
+ }
+
+ static function mLon($t, $n): float
+ {
+ $ob = self::$XL1;
+ $obl = count($ob[0]);
+ $tn = 1;
+ $v = 0;
+ $t2 = $t * $t;
+ $t3 = $t2 * $t;
+ $t4 = $t3 * $t;
+ $t5 = $t4 * $t;
+ $tx = $t - 10;
+ $v += (3.81034409 + 8399.684730072 * $t - 3.319e-05 * $t2 + 3.11e-08 * $t3 - 2.033e-10 * $t4) * self::$SECOND_PER_RAD;
+ $v += 5028.792262 * $t + 1.1124406 * $t2 + 0.00007699 * $t3 - 0.000023479 * $t4 - 0.0000000178 * $t5;
+ if ($tx > 0) {
+ $v += -0.866 + 1.43 * $tx + 0.054 * $tx * $tx;
+ }
+ $t2 /= 1e4;
+ $t3 /= 1e8;
+ $t4 /= 1e8;
+
+ $n *= 6;
+ if ($n < 0) {
+ $n = $obl;
+ }
+ for ($i = 0, $x = count($ob); $i < $x; $i++, $tn *= $t) {
+ $f = $ob[$i];
+ $l = count($f);
+ $m = (int)($n * $l / $obl + 0.5);
+ if ($i > 0) {
+ $m += 6;
+ }
+ if ($m >= $l) {
+ $m = $l;
+ }
+ for ($j = 0, $c = 0; $j < $m; $j += 6) {
+ $c += $f[$j] * cos($f[$j + 1] + $t * $f[$j + 2] + $t2 * $f[$j + 3] + $t3 * $f[$j + 4] + $t4 * $f[$j + 5]);
+ }
+ $v += $c * $tn;
+ }
+ $v /= self::$SECOND_PER_RAD;
+ return $v;
+ }
+
+ static function gxcSunLon($t): float
+ {
+ $t2 = $t * $t;
+ $v = -0.043126 + 628.301955 * $t - 0.000002732 * $t2;
+ $e = 0.016708634 - 0.000042037 * $t - 0.0000001267 * $t2;
+ return -20.49552 * (1 + $e * cos($v)) / self::$SECOND_PER_RAD;
+ }
+
+ static function ev($t): float
+ {
+ $f = 628.307585 * $t;
+ return 628.332 + 21 * sin(1.527 + $f) + 0.44 * sin(1.48 + $f * 2) + 0.129 * sin(5.82 + $f) * $t + 0.00055 * sin(4.21 + $f) * $t * $t;
+ }
+
+ static function saLon($t, $n)
+ {
+ return self::eLon($t, $n) + self::nutationLon2($t) + self::gxcSunLon($t) + M_PI;
+ }
+
+ static function dtExt($y, $jsd): float
+ {
+ $dy = ($y - 1820) / 100;
+ return -20 + $jsd * $dy * $dy;
+ }
+
+ static function dtCalc($y)
+ {
+ $size = count(self::$DT_AT);
+ $y0 = self::$DT_AT[$size - 2];
+ $t0 = self::$DT_AT[$size - 1];
+ if ($y >= $y0) {
+ $jsd = 31;
+ if ($y > $y0 + 100) {
+ return self::dtExt($y, $jsd);
+ }
+ return self::dtExt($y, $jsd) - (self::dtExt($y0, $jsd) - $t0) * ($y0 + 100 - $y) / 100;
+ }
+ for ($i = 0; $i < $size; $i += 5) {
+ if ($y < self::$DT_AT[$i + 5]) {
+ break;
+ }
+ }
+ $t1 = ($y - self::$DT_AT[$i]) / (self::$DT_AT[$i + 5] - self::$DT_AT[$i]) * 10;
+ $t2 = $t1 * $t1;
+ $t3 = $t2 * $t1;
+ return self::$DT_AT[$i + 1] + self::$DT_AT[$i + 2] * $t1 + self::$DT_AT[$i + 3] * $t2 + self::$DT_AT[$i + 4] * $t3;
+ }
+
+ static function dtT($t): float
+ {
+ return self::dtCalc($t / 365.2425 + 2000) / self::$SECOND_PER_DAY;
+ }
+
+ static function mv($t): float
+ {
+ $v = 8399.71 - 914 * sin(0.7848 + 8328.691425 * $t + 0.0001523 * $t * $t);
+ $v -= 179 * sin(2.543 + 15542.7543 * $t) + 160 * sin(0.1874 + 7214.0629 * $t) + 62 * sin(3.14 + 16657.3828 * $t) + 34 * sin(4.827 + 16866.9323 * $t) + 22 * sin(4.9 + 23871.4457 * $t) + 12 * sin(2.59 + 14914.4523 * $t) + 7 * sin(0.23 + 6585.7609 * $t) + 5 * sin(0.9 + 25195.624 * $t) + 5 * sin(2.32 - 7700.3895 * $t) + 5 * sin(3.88 + 8956.9934 * $t) + 5 * sin(0.49 + 7771.3771 * $t);
+ return $v;
+ }
+
+ static function saLonT($w): float
+ {
+ $v = 628.3319653318;
+ $t = ($w - 1.75347 - M_PI) / $v;
+ $v = self::ev($t);
+ $t += ($w - self::saLon($t, 10)) / $v;
+ $v = self::ev($t);
+ $t += ($w - self::saLon($t, -1)) / $v;
+ return $t;
+ }
+
+ static function saLonT2($w): float
+ {
+ $v = 628.3319653318;
+ $t = ($w - 1.75347 - M_PI) / $v;
+ $t -= (0.000005297 * $t * $t + 0.0334166 * cos(4.669257 + 628.307585 * $t) + 0.0002061 * cos(2.67823 + 628.307585 * $t) * $t) / $v;
+ $t += ($w - self::eLon($t, 8) - M_PI + (20.5 + 17.2 * sin(2.1824 - 33.75705 * $t)) / self::$SECOND_PER_RAD) / $v;
+ return $t;
+ }
+
+ static function msaLon($t, $mn, $sn): float
+ {
+ return self::mLon($t, $mn) + (-3.4E-6) - (self::eLon($t, $sn) + self::gxcSunLon($t) + M_PI);
+ }
+
+ static function msaLonT($w): float
+ {
+ $v = 7771.37714500204;
+ $t = ($w + 1.08472) / $v;
+ $t += ($w - self::msaLon($t, 3, 3)) / $v;
+ $v = self::mv($t) - self::ev($t);
+ $t += ($w - self::msaLon($t, 20, 10)) / $v;
+ $t += ($w - self::msaLon($t, -1, 60)) / $v;
+ return $t;
+ }
+
+ static function msaLonT2($w): float
+ {
+ $v = 7771.37714500204;
+ $t = ($w + 1.08472) / $v;
+ $t2 = $t * $t;
+ $t -= (-0.00003309 * $t2 + 0.10976 * cos(0.784758 + 8328.6914246 * $t + 0.000152292 * $t2) + 0.02224 * cos(0.18740 + 7214.0628654 * $t - 0.00021848 * $t2) - 0.03342 * cos(4.669257 + 628.307585 * $t)) / $v;
+ $l = self::mLon($t, 20) - (4.8950632 + 628.3319653318 * $t + 0.000005297 * $t * $t + 0.0334166 * cos(4.669257 + 628.307585 * $t) + 0.0002061 * cos(2.67823 + 628.307585 * $t) * $t + 0.000349 * cos(4.6261 + 1256.61517 * $t) - 20.5 / self::$SECOND_PER_RAD);
+ $v = 7771.38 - 914 * sin(0.7848 + 8328.691425 * $t + 0.0001523 * $t * $t) - 179 * sin(2.543 + 15542.7543 * $t) - 160 * sin(0.1874 + 7214.0629 * $t);
+ $t += ($w - $l) / $v;
+ return $t;
+ }
+
+ static function qiHigh($w): float
+ {
+ $t = self::saLonT2($w) * 36525;
+ $t = $t - self::dtT($t) + self::$ONE_THIRD;
+ $v = ((int)($t + 0.5) % 1) * self::$SECOND_PER_DAY;
+ if ($v < 1200 || $v > self::$SECOND_PER_DAY - 1200) {
+ $t = self::saLonT($w) * 36525 - self::dtT($t) + self::$ONE_THIRD;
+ }
+ return $t;
+ }
+
+ static function shuoHigh($w): float
+ {
+ $t = self::msaLonT2($w) * 36525;
+ $t = $t - self::dtT($t) + self::$ONE_THIRD;
+ $v = ((int)($t + 0.5) % 1) * self::$SECOND_PER_DAY;
+ if ($v < 1800 || $v > self::$SECOND_PER_DAY - 1800) {
+ $t = self::msaLont($w) * 36525 - self::dtT($t) + self::$ONE_THIRD;
+ }
+ return $t;
+ }
+
+ static function qiLow($w): float
+ {
+ $v = 628.3319653318;
+ $t = ($w - 4.895062166) / $v;
+ $t -= (53 * $t * $t + 334116 * cos(4.67 + 628.307585 * $t) + 2061 * cos(2.678 + 628.3076 * $t) * $t) / $v / 10000000;
+ $n = 48950621.66 + 6283319653.318 * $t + 53 * $t * $t + 334166 * cos(4.669257 + 628.307585 * $t) + 3489 * cos(4.6261 + 1256.61517 * $t) + 2060.6 * cos(2.67823 + 628.307585 * $t) * $t - 994 - 834 * sin(2.1824 - 33.75705 * $t);
+ $t -= ($n / 10000000 - $w) / 628.332 + (32 * ($t + 1.8) * ($t + 1.8) - 20) / self::$SECOND_PER_DAY / 36525;
+ return $t * 36525 + self::$ONE_THIRD;
+ }
+
+ static function shuoLow($w): float
+ {
+ $v = 7771.37714500204;
+ $t = ($w + 1.08472) / $v;
+ $t -= (-0.0000331 * $t * $t + 0.10976 * cos(0.785 + 8328.6914 * $t) + 0.02224 * cos(0.187 + 7214.0629 * $t) - 0.03342 * cos(4.669 + 628.3076 * $t)) / $v + (32 * ($t + 1.8) * ($t + 1.8) - 20) / self::$SECOND_PER_DAY / 36525;
+ return $t * 36525 + self::$ONE_THIRD;
+ }
+
+ static function calcShuo($jd): float
+ {
+ if (null == self::$SB) {
+ self::$SB = self::decode('EqoFscDcrFpmEsF2DfFideFelFpFfFfFiaipqti1ksttikptikqckstekqttgkqttgkqteksttikptikq2fjstgjqttjkqttgkqtekstfkptikq2tijstgjiFkirFsAeACoFsiDaDiADc1AFbBfgdfikijFifegF1FhaikgFag1E2btaieeibggiffdeigFfqDfaiBkF1kEaikhkigeidhhdiegcFfakF1ggkidbiaedksaFffckekidhhdhdikcikiakicjF1deedFhFccgicdekgiFbiaikcfi1kbFibefgEgFdcFkFeFkdcfkF1kfkcickEiFkDacFiEfbiaejcFfffkhkdgkaiei1ehigikhdFikfckF1dhhdikcfgjikhfjicjicgiehdikcikggcifgiejF1jkieFhegikggcikFegiegkfjebhigikggcikdgkaFkijcfkcikfkcifikiggkaeeigefkcdfcfkhkdgkegieidhijcFfakhfgeidieidiegikhfkfckfcjbdehdikggikgkfkicjicjF1dbidikFiggcifgiejkiegkigcdiegfggcikdbgfgefjF1kfegikggcikdgFkeeijcfkcikfkekcikdgkabhkFikaffcfkhkdgkegbiaekfkiakicjhfgqdq2fkiakgkfkhfkfcjiekgFebicggbedF1jikejbbbiakgbgkacgiejkijjgigfiakggfggcibFifjefjF1kfekdgjcibFeFkijcfkfhkfkeaieigekgbhkfikidfcjeaibgekgdkiffiffkiakF1jhbakgdki1dj1ikfkicjicjieeFkgdkicggkighdF1jfgkgfgbdkicggfggkidFkiekgijkeigfiskiggfaidheigF1jekijcikickiggkidhhdbgcfkFikikhkigeidieFikggikhkffaffijhidhhakgdkhkijF1kiakF1kfheakgdkifiggkigicjiejkieedikgdfcggkigieeiejfgkgkigbgikicggkiaideeijkefjeijikhkiggkiaidheigcikaikffikijgkiahi1hhdikgjfifaakekighie1hiaikggikhkffakicjhiahaikggikhkijF1kfejfeFhidikggiffiggkigicjiekgieeigikggiffiggkidheigkgfjkeigiegikifiggkidhedeijcfkFikikhkiggkidhh1ehigcikaffkhkiggkidhh1hhigikekfiFkFikcidhh1hitcikggikhkfkicjicghiediaikggikhkijbjfejfeFhaikggifikiggkigiejkikgkgieeigikggiffiggkigieeigekijcijikggifikiggkideedeijkefkfckikhkiggkidhh1ehijcikaffkhkiggkidhh1hhigikhkikFikfckcidhh1hiaikgjikhfjicjicgiehdikcikggifikigiejfejkieFhegikggifikiggfghigkfjeijkhigikggifikiggkigieeijcijcikfksikifikiggkidehdeijcfdckikhkiggkhghh1ehijikifffffkhsFngErD1pAfBoDd1BlEtFqA2AqoEpDqElAEsEeB2BmADlDkqBtC1FnEpDqnEmFsFsAFnllBbFmDsDiCtDmAB2BmtCgpEplCpAEiBiEoFqFtEqsDcCnFtADnFlEgdkEgmEtEsCtDmADqFtAFrAtEcCqAE1BoFqC1F1DrFtBmFtAC2ACnFaoCgADcADcCcFfoFtDlAFgmFqBq2bpEoAEmkqnEeCtAE1bAEqgDfFfCrgEcBrACfAAABqAAB1AAClEnFeCtCgAADqDoBmtAAACbFiAAADsEtBqAB2FsDqpFqEmFsCeDtFlCeDtoEpClEqAAFrAFoCgFmFsFqEnAEcCqFeCtFtEnAEeFtAAEkFnErAABbFkADnAAeCtFeAfBoAEpFtAABtFqAApDcCGJ');
+ }
+ $size = count(self::$SHUO_KB);
+ $d = 0;
+ $pc = 14;
+ $jd += 2451545;
+ $f1 = self::$SHUO_KB[0] - $pc;
+ $f2 = self::$SHUO_KB[$size - 1] - $pc;
+ $f3 = 2436935;
+ if ($jd < $f1 || $jd >= $f3) {
+ $d = floor(self::shuoHigh(floor(($jd + $pc - 2451551) / 29.5306) * M_PI * 2) + 0.5);
+ } else if ($jd >= $f1 && $jd < $f2) {
+ for ($i = 0; $i < $size; $i += 2) {
+ if ($jd + $pc < self::$SHUO_KB[$i + 2]) {
+ break;
+ }
+ }
+ $d = self::$SHUO_KB[$i] + self::$SHUO_KB[$i + 1] * floor(($jd + $pc - self::$SHUO_KB[$i]) / self::$SHUO_KB[$i + 1]);
+ $d = floor($d + 0.5);
+ if ($d == 1683460) {
+ $d++;
+ }
+ $d -= 2451545;
+ } else if ($jd >= $f2) {
+ $d = floor(self::shuoLow(floor(($jd + $pc - 2451551) / 29.5306) * M_PI * 2) + 0.5);
+ $from = (int)(($jd - $f2) / 29.5306);
+ $n = substr(self::$SB, $from, 1);
+ if (strcmp('1', $n) == 0) {
+ $d += 1;
+ } elseif (strcmp('2', $n) == 0) {
+ $d -= 1;
+ }
+ }
+ return $d;
+ }
+
+ static function calcQi($jd): float
+ {
+ if (null == self::$QB) {
+ self::$QB = self::decode('FrcFs22AFsckF2tsDtFqEtF1posFdFgiFseFtmelpsEfhkF2anmelpFlF1ikrotcnEqEq2FfqmcDsrFor22FgFrcgDscFs22FgEeFtE2sfFs22sCoEsaF2tsD1FpeE2eFsssEciFsFnmelpFcFhkF2tcnEqEpFgkrotcnEqrEtFermcDsrE222FgBmcmr22DaEfnaF222sD1FpeForeF2tssEfiFpEoeFssD1iFstEqFppDgFstcnEqEpFg11FscnEqrAoAF2ClAEsDmDtCtBaDlAFbAEpAAAAAD2FgBiBqoBbnBaBoAAAAAAAEgDqAdBqAFrBaBoACdAAf1AACgAAAeBbCamDgEifAE2AABa1C1BgFdiAAACoCeE1ADiEifDaAEqAAFe1AcFbcAAAAAF1iFaAAACpACmFmAAAAAAAACrDaAAADG0');
+ }
+ $size = count(self::$QI_KB);
+ $d = 0;
+ $pc = 7;
+ $jd += 2451545;
+ $f1 = self::$QI_KB[0] - $pc;
+ $f2 = self::$QI_KB[$size - 1] - $pc;
+ $f3 = 2436935;
+ if ($jd < $f1 || $jd >= $f3) {
+ $d = floor(self::qiHigh(floor(($jd + $pc - 2451259) / self::$DAY_PER_YEAR * 24) * M_PI / 12) + 0.5);
+ } else if ($jd >= $f1 && $jd < $f2) {
+ for ($i = 0; $i < $size; $i += 2) {
+ if ($jd + $pc < self::$QI_KB[$i + 2]) {
+ break;
+ }
+ }
+ $d = self::$QI_KB[$i] + self::$QI_KB[$i + 1] * floor(($jd + $pc - self::$QI_KB[$i]) / self::$QI_KB[$i + 1]);
+ $d = floor($d + 0.5);
+ if ($d == 1683460) {
+ $d++;
+ }
+ $d -= 2451545;
+ } else if ($jd >= $f2) {
+ $d = floor(self::qiLow(floor(($jd + $pc - 2451259) / self::$DAY_PER_YEAR * 24) * M_PI / 12) + 0.5);
+ $from = (int)(($jd - $f2) / self::$DAY_PER_YEAR * 24);
+ $n = substr(self::$QB, $from, 1);
+ if (strcmp('1', $n) == 0) {
+ $d += 1;
+ } elseif (strcmp('2', $n) == 0) {
+ $d -= 1;
+ }
+ }
+ return $d;
+ }
+
+ static function qiAccurate($w): float
+ {
+ $t = self::saLonT($w) * 36525;
+ return $t - self::dtT($t) + self::$ONE_THIRD;
+ }
+
+ static function qiAccurate2($jd): float
+ {
+ $d = M_PI / 12;
+ $w = floor(($jd + 293) / self::$DAY_PER_YEAR * 24) * $d;
+ $a = self::qiAccurate($w);
+ if ($a - $jd > 5) {
+ return self::qiAccurate($w - $d);
+ }
+ if ($a - $jd < -5) {
+ return self::qiAccurate($w + $d);
+ }
+ return $a;
+ }
+
+}
diff --git a/test/ConstellationTest.php b/test/ConstellationTest.php
new file mode 100644
index 0000000..99f53e2
--- /dev/null
+++ b/test/ConstellationTest.php
@@ -0,0 +1,35 @@
+assertEquals('白羊', SolarDay::fromYmd(2020, 3, 21)->getConstellation()->getName());
+ $this->assertEquals('白羊', SolarDay::fromYmd(2020, 4, 19)->getConstellation()->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('金牛', SolarDay::fromYmd(2020, 4, 20)->getConstellation()->getName());
+ $this->assertEquals('金牛', SolarDay::fromYmd(2020, 5, 20)->getConstellation()->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('双子', SolarDay::fromYmd(2020, 5, 21)->getConstellation()->getName());
+ $this->assertEquals('双子', SolarDay::fromYmd(2020, 6, 21)->getConstellation()->getName());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('巨蟹', SolarDay::fromYmd(2020, 6, 22)->getConstellation()->getName());
+ $this->assertEquals('巨蟹', SolarDay::fromYmd(2020, 7, 22)->getConstellation()->getName());
+ }
+}
diff --git a/test/DirectionTest.php b/test/DirectionTest.php
new file mode 100644
index 0000000..a6634f9
--- /dev/null
+++ b/test/DirectionTest.php
@@ -0,0 +1,36 @@
+assertEquals('东南', SolarDay::fromYmd(2021, 11, 13)->getLunarDay()->getSixtyCycle()->getHeavenStem()->getMascotDirection()->getName());
+ }
+
+ /**
+ * 福神方位
+ */
+ function test2()
+ {
+ $this->assertEquals('东南', SolarDay::fromYmd(2024, 1, 1)->getLunarDay()->getSixtyCycle()->getHeavenStem()->getMascotDirection()->getName());
+ }
+
+ /**
+ * 太岁方位
+ */
+ function test3()
+ {
+ $this->assertEquals('东', SolarDay::fromYmd(2023, 11, 6)->getLunarDay()->getJupiterDirection()->getName());
+ }
+
+}
diff --git a/test/DogDayTest.php b/test/DogDayTest.php
new file mode 100644
index 0000000..7aaf210
--- /dev/null
+++ b/test/DogDayTest.php
@@ -0,0 +1,103 @@
+getDogDay();
+ $this->assertEquals('初伏', $d->getName());
+ $this->assertEquals('初伏', $d->getDog()->__toString());
+ $this->assertEquals('初伏第1天', $d->__toString());
+ }
+
+
+ function test1()
+ {
+ $d = SolarDay::fromYmd(2011, 7, 23)->getDogDay();
+ $this->assertEquals('初伏', $d->getName());
+ $this->assertEquals('初伏', $d->getDog()->__toString());
+ $this->assertEquals('初伏第10天', $d->__toString());
+ }
+
+
+ function test2()
+ {
+ $d = SolarDay::fromYmd(2011, 7, 24)->getDogDay();
+ $this->assertEquals('中伏', $d->getName());
+ $this->assertEquals('中伏', $d->getDog()->__toString());
+ $this->assertEquals('中伏第1天', $d->__toString());
+ }
+
+
+ function test3()
+ {
+ $d = SolarDay::fromYmd(2011, 8, 12)->getDogDay();
+ $this->assertEquals('中伏', $d->getName());
+ $this->assertEquals('中伏', $d->getDog()->__toString());
+ $this->assertEquals('中伏第20天', $d->__toString());
+ }
+
+
+ function test4()
+ {
+ $d = SolarDay::fromYmd(2011, 8, 13)->getDogDay();
+ $this->assertEquals('末伏', $d->getName());
+ $this->assertEquals('末伏', $d->getDog()->__toString());
+ $this->assertEquals('末伏第1天', $d->__toString());
+ }
+
+
+ function test5()
+ {
+ $d = SolarDay::fromYmd(2011, 8, 22)->getDogDay();
+ $this->assertEquals('末伏', $d->getName());
+ $this->assertEquals('末伏', $d->getDog()->__toString());
+ $this->assertEquals('末伏第10天', $d->__toString());
+ }
+
+
+ function test6()
+ {
+ $this->assertNull(SolarDay::fromYmd(2011, 7, 13)->getDogDay());
+ }
+
+
+ function test7()
+ {
+ $this->assertNull(SolarDay::fromYmd(2011, 8, 23)->getDogDay());
+ }
+
+
+ function test8()
+ {
+ $d = SolarDay::fromYmd(2012, 7, 18)->getDogDay();
+ $this->assertEquals('初伏', $d->getName());
+ $this->assertEquals('初伏', $d->getDog()->__toString());
+ $this->assertEquals('初伏第1天', $d->__toString());
+ }
+
+
+ function test9()
+ {
+ $d = SolarDay::fromYmd(2012, 8, 5)->getDogDay();
+ $this->assertEquals('中伏', $d->getName());
+ $this->assertEquals('中伏', $d->getDog()->__toString());
+ $this->assertEquals('中伏第9天', $d->__toString());
+ }
+
+
+ function test10()
+ {
+ $d = SolarDay::fromYmd(2012, 8, 8)->getDogDay();
+ $this->assertEquals('末伏', $d->getName());
+ $this->assertEquals('末伏', $d->getDog()->__toString());
+ $this->assertEquals('末伏第2天', $d->__toString());
+ }
+}
diff --git a/test/DutyTest.php b/test/DutyTest.php
new file mode 100644
index 0000000..ef9e0ed
--- /dev/null
+++ b/test/DutyTest.php
@@ -0,0 +1,31 @@
+assertEquals('闭', SolarDay::fromYmd(2023, 10, 30)->getLunarDay()->getDuty()->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('建', SolarDay::fromYmd(2023, 10, 19)->getLunarDay()->getDuty()->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('除', SolarDay::fromYmd(2023, 10, 7)->getLunarDay()->getDuty()->getName());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('除', SolarDay::fromYmd(2023, 10, 8)->getLunarDay()->getDuty()->getName());
+ }
+}
diff --git a/test/EarthlyBranchTest.php b/test/EarthlyBranchTest.php
new file mode 100644
index 0000000..8fb03d3
--- /dev/null
+++ b/test/EarthlyBranchTest.php
@@ -0,0 +1,21 @@
+assertEquals('子', EarthBranch::fromIndex(0)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals(0, EarthBranch::fromName('子')->getIndex());
+ }
+}
diff --git a/test/EclipticTest.php b/test/EclipticTest.php
new file mode 100644
index 0000000..68f0e24
--- /dev/null
+++ b/test/EclipticTest.php
@@ -0,0 +1,43 @@
+getLunarDay()->getTwelveStar();
+ $this->assertEquals('天德', $star->getName());
+ $this->assertEquals('黄道', $star->getEcliptic()->getName());
+ $this->assertEquals('吉', $star->getEcliptic()->getLuck()->getName());
+ }
+
+ function test1()
+ {
+ $star = SolarDay::fromYmd(2023, 10, 19)->getLunarDay()->getTwelveStar();
+ $this->assertEquals('白虎', $star->getName());
+ $this->assertEquals('黑道', $star->getEcliptic()->getName());
+ $this->assertEquals('凶', $star->getEcliptic()->getLuck()->getName());
+ }
+
+ function test2()
+ {
+ $star = SolarDay::fromYmd(2023, 10, 7)->getLunarDay()->getTwelveStar();
+ $this->assertEquals('天牢', $star->getName());
+ $this->assertEquals('黑道', $star->getEcliptic()->getName());
+ $this->assertEquals('凶', $star->getEcliptic()->getLuck()->getName());
+ }
+
+ function test3()
+ {
+ $star = SolarDay::fromYmd(2023, 10, 8)->getLunarDay()->getTwelveStar();
+ $this->assertEquals('玉堂', $star->getName());
+ $this->assertEquals('黄道', $star->getEcliptic()->getName());
+ $this->assertEquals('吉', $star->getEcliptic()->getLuck()->getName());
+ }
+}
diff --git a/test/EightCharTest.php b/test/EightCharTest.php
new file mode 100644
index 0000000..bf9b05a
--- /dev/null
+++ b/test/EightCharTest.php
@@ -0,0 +1,496 @@
+getYear();
+ // 月柱
+ $month = $eightChar->getMonth();
+ // 日柱
+ $day = $eightChar->getDay();
+ // 时柱
+ $hour = $eightChar->getHour();
+
+ // 日元(日主、日干)
+ $me = $day->getHeavenStem();
+
+ // 年柱天干十神
+ $this->assertEquals('正财', $me->getTenStar($year->getHeavenStem())->getName());
+ // 月柱天干十神
+ $this->assertEquals('比肩', $me->getTenStar($month->getHeavenStem())->getName());
+ // 时柱天干十神
+ $this->assertEquals('七杀', $me->getTenStar($hour->getHeavenStem())->getName());
+
+ // 年柱地支十神(本气)
+ $this->assertEquals('伤官', $me->getTenStar($year->getEarthBranch()->getHideHeavenStemMain())->getName());
+ // 年柱地支十神(中气)
+ $this->assertEquals('正财', $me->getTenStar($year->getEarthBranch()->getHideHeavenStemMiddle())->getName());
+ // 年柱地支十神(余气)
+ $this->assertEquals('正官', $me->getTenStar($year->getEarthBranch()->getHideHeavenStemResidual())->getName());
+
+ // 日柱地支十神(本气)
+ $this->assertEquals('偏印', $me->getTenStar($day->getEarthBranch()->getHideHeavenStemMain())->getName());
+ // 日柱地支藏干(中气)
+ $this->assertNull($day->getEarthBranch()->getHideHeavenStemMiddle());
+ // 日柱地支藏干(余气)
+ $this->assertNull($day->getEarthBranch()->getHideHeavenStemResidual());
+
+ // 指定任意天干的十神
+ $this->assertEquals('正财', $me->getTenStar(HeavenStem::fromName('丙'))->getName());
+ }
+
+ /**
+ * 地势(长生十二神)
+ */
+ function test2()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('丙寅'),
+ SixtyCycle::fromName('癸巳'),
+ SixtyCycle::fromName('癸酉'),
+ SixtyCycle::fromName('己未')
+ );
+
+ // 年柱
+ $year = $eightChar->getYear();
+ // 月柱
+ $month = $eightChar->getMonth();
+ // 日柱
+ $day = $eightChar->getDay();
+ // 时柱
+ $hour = $eightChar->getHour();
+
+ // 日元(日主、日干)
+ $me = $day->getHeavenStem();
+
+ // 年柱地势
+ $this->assertEquals('沐浴', $me->getTerrain($year->getEarthBranch())->getName());
+ // 月柱地势
+ $this->assertEquals('胎', $me->getTerrain($month->getEarthBranch())->getName());
+ // 日柱地势
+ $this->assertEquals('病', $me->getTerrain($day->getEarthBranch())->getName());
+ // 时柱地势
+ $this->assertEquals('墓', $me->getTerrain($hour->getEarthBranch())->getName());
+ }
+
+ /**
+ * 胎元/胎息/命宫
+ */
+ function test3()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('癸卯'),
+ SixtyCycle::fromName('辛酉'),
+ SixtyCycle::fromName('己亥'),
+ SixtyCycle::fromName('癸酉')
+ );
+
+ // 胎元
+ $taiYuan = $eightChar->getFetalOrigin();
+ $this->assertEquals('壬子', $taiYuan->getName());
+ // 胎元纳音
+ $this->assertEquals('桑柘木', $taiYuan->getSound()->getName());
+ }
+
+ /**
+ * 胎息
+ */
+ function test4()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('癸卯'),
+ SixtyCycle::fromName('辛酉'),
+ SixtyCycle::fromName('己亥'),
+ SixtyCycle::fromName('癸酉')
+ );
+
+ // 胎息
+ $taiXi = $eightChar->getFetalBreath();
+ $this->assertEquals('甲寅', $taiXi->getName());
+ // 胎息纳音
+ $this->assertEquals('大溪水', $taiXi->getSound()->getName());
+ }
+
+ /**
+ * 命宫
+ */
+ function test5()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('癸卯'),
+ SixtyCycle::fromName('辛酉'),
+ SixtyCycle::fromName('己亥'),
+ SixtyCycle::fromName('癸酉')
+ );
+
+ // 命宫
+ $mingGong = $eightChar->getOwnSign();
+ $this->assertEquals('癸亥', $mingGong->getName());
+ // 命宫纳音
+ $this->assertEquals('大海水', $mingGong->getSound()->getName());
+ }
+
+ /**
+ * 身宫
+ */
+ function test6()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('癸卯'),
+ SixtyCycle::fromName('辛酉'),
+ SixtyCycle::fromName('己亥'),
+ SixtyCycle::fromName('癸酉')
+ );
+
+ // 身宫
+ $shenGong = $eightChar->getBodySign();
+ $this->assertEquals('己未', $shenGong->getName());
+ // 身宫纳音
+ $this->assertEquals('天上火', $shenGong->getSound()->getName());
+ }
+
+ /**
+ * 地势(长生十二神)
+ */
+ function test7()
+ {
+ // 八字
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('乙酉'),
+ SixtyCycle::fromName('戊子'),
+ SixtyCycle::fromName('辛巳'),
+ SixtyCycle::fromName('壬辰')
+ );
+
+ // 日干
+ $me = $eightChar->getDay()->getHeavenStem();
+ // 年柱地势
+ $this->assertEquals('临官', $me->getTerrain($eightChar->getYear()->getEarthBranch())->getName());
+ // 月柱地势
+ $this->assertEquals('长生', $me->getTerrain($eightChar->getMonth()->getEarthBranch())->getName());
+ // 日柱地势
+ $this->assertEquals('死', $me->getTerrain($eightChar->getDay()->getEarthBranch())->getName());
+ // 时柱地势
+ $this->assertEquals('墓', $me->getTerrain($eightChar->getHour()->getEarthBranch())->getName());
+ }
+
+ /**
+ * 公历时刻转八字
+ */
+ function test8()
+ {
+ $eightChar = SolarTime::fromYmdHms(2005, 12, 23, 8, 37, 0)->getLunarHour()->getEightChar();
+ $this->assertEquals('乙酉', $eightChar->getYear()->getName());
+ $this->assertEquals('戊子', $eightChar->getMonth()->getName());
+ $this->assertEquals('辛巳', $eightChar->getDay()->getName());
+ $this->assertEquals('壬辰', $eightChar->getHour()->getName());
+ }
+
+ function test9()
+ {
+ $eightChar = SolarTime::fromYmdHms(1988, 2, 15, 23, 30, 0)->getLunarHour()->getEightChar();
+ $this->assertEquals('戊辰', $eightChar->getYear()->getName());
+ $this->assertEquals('甲寅', $eightChar->getMonth()->getName());
+ $this->assertEquals('辛丑', $eightChar->getDay()->getName());
+ $this->assertEquals('戊子', $eightChar->getHour()->getName());
+ }
+
+ /**
+ * 童限测试
+ */
+ function test11()
+ {
+ $childLimit = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(2022, 3, 9, 20, 51, 0), Gender::MAN);
+ $this->assertEquals(8, $childLimit->getYearCount());
+ $this->assertEquals(9, $childLimit->getMonthCount());
+ $this->assertEquals(2, $childLimit->getDayCount());
+ $this->assertEquals(10, $childLimit->getHourCount());
+ $this->assertEquals(26, $childLimit->getMinuteCount());
+ $this->assertEquals('2030年12月12日 07:17:00', $childLimit->getEndTime()->__toString());
+ }
+
+ /**
+ * 童限测试
+ */
+ function test12()
+ {
+ $childLimit = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(2018, 6, 11, 9, 30, 0), Gender::WOMAN);
+ $this->assertEquals(1, $childLimit->getYearCount());
+ $this->assertEquals(9, $childLimit->getMonthCount());
+ $this->assertEquals(10, $childLimit->getDayCount());
+ $this->assertEquals(1, $childLimit->getHourCount());
+ $this->assertEquals(42, $childLimit->getMinuteCount());
+ $this->assertEquals('2020年3月21日 11:12:00', $childLimit->getEndTime()->__toString());
+ }
+
+ /**
+ * 大运测试
+ */
+ function test13()
+ {
+ // 童限
+ $childLimit = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(1983, 2, 15, 20, 0, 0), Gender::WOMAN);
+ // 八字
+ $this->assertEquals('癸亥 甲寅 甲戌 甲戌', $childLimit->getEightChar()->__toString());
+ // 童限年数
+ $this->assertEquals(6, $childLimit->getYearCount());
+ // 童限月数
+ $this->assertEquals(2, $childLimit->getMonthCount());
+ // 童限日数
+ $this->assertEquals(18, $childLimit->getDayCount());
+ // 童限结束(即开始起运)的公历时刻
+ $this->assertEquals('1989年5月4日 18:24:00', $childLimit->getEndTime()->__toString());
+ // 童限开始(即出生)的农历年干支
+ $this->assertEquals('癸亥', $childLimit->getStartTime()->getLunarHour()->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ // 童限结束(即开始起运)的农历年干支
+ $this->assertEquals('己巳', $childLimit->getEndTime()->getLunarHour()->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+
+ // 第1轮大运
+ $decadeFortune = $childLimit->getStartDecadeFortune();
+ // 开始年龄
+ $this->assertEquals(7, $decadeFortune->getStartAge());
+ // 结束年龄
+ $this->assertEquals(16, $decadeFortune->getEndAge());
+ // 开始年
+ $this->assertEquals(1989, $decadeFortune->getStartLunarYear()->getYear());
+ // 结束年
+ $this->assertEquals(1998, $decadeFortune->getEndLunarYear()->getYear());
+ // 干支
+ $this->assertEquals('乙卯', $decadeFortune->getName());
+ // 下一大运
+ $this->assertEquals('丙辰', $decadeFortune->next(1)->getName());
+ // 上一大运
+ $this->assertEquals('甲寅', $decadeFortune->next(-1)->getName());
+ // 第9轮大运
+ $this->assertEquals('癸亥', $decadeFortune->next(8)->getName());
+
+ // 小运
+ $fortune = $childLimit->getStartFortune();
+ // 年龄
+ $this->assertEquals(7, $fortune->getAge());
+ // 农历年
+ $this->assertEquals(1989, $fortune->getLunarYear()->getYear());
+ // 干支
+ $this->assertEquals('辛巳', $fortune->getName());
+
+ // 流年
+ $this->assertEquals('己巳', $fortune->getLunarYear()->getSixtyCycle()->getName());
+ }
+
+ function test14()
+ {
+ // 童限
+ $childLimit = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(1992, 2, 2, 12, 0, 0), Gender::MAN);
+ // 八字
+ $this->assertEquals('辛未 辛丑 戊申 戊午', $childLimit->getEightChar()->__toString());
+ // 童限年数
+ $this->assertEquals(9, $childLimit->getYearCount());
+ // 童限月数
+ $this->assertEquals(0, $childLimit->getMonthCount());
+ // 童限日数
+ $this->assertEquals(9, $childLimit->getDayCount());
+ // 童限结束(即开始起运)的公历时刻
+ $this->assertEquals('2001年2月11日 18:58:00', $childLimit->getEndTime()->__toString());
+ // 童限开始(即出生)的农历年干支
+ $this->assertEquals('辛未', $childLimit->getStartTime()->getLunarHour()->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ // 童限结束(即开始起运)的农历年干支
+ $this->assertEquals('辛巳', $childLimit->getEndTime()->getLunarHour()->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+
+ // 第1轮大运
+ $decadeFortune = $childLimit->getStartDecadeFortune();
+ // 开始年龄
+ $this->assertEquals(10, $decadeFortune->getStartAge());
+ // 结束年龄
+ $this->assertEquals(19, $decadeFortune->getEndAge());
+ // 开始年
+ $this->assertEquals(2001, $decadeFortune->getStartLunarYear()->getYear());
+ // 结束年
+ $this->assertEquals(2010, $decadeFortune->getEndLunarYear()->getYear());
+ // 干支
+ $this->assertEquals('庚子', $decadeFortune->getName());
+ // 下一大运
+ $this->assertEquals('己亥', $decadeFortune->next(1)->getName());
+
+ // 小运
+ $fortune = $childLimit->getStartFortune();
+ // 年龄
+ $this->assertEquals(10, $fortune->getAge());
+ // 农历年
+ $this->assertEquals(2001, $fortune->getLunarYear()->getYear());
+ // 干支
+ $this->assertEquals('戊申', $fortune->getName());
+ // 小运推移
+ $this->assertEquals('丙午', $fortune->next(2)->getName());
+ $this->assertEquals('庚戌', $fortune->next(-2)->getName());
+
+ // 流年
+ $this->assertEquals('辛巳', $fortune->getLunarYear()->getSixtyCycle()->getName());
+ }
+
+ function test16()
+ {
+ // 童限
+ $childLimit = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(1990, 3, 15, 10, 30, 0), Gender::MAN);
+ // 八字
+ $this->assertEquals('庚午 己卯 己卯 己巳', $childLimit->getEightChar()->__toString());
+ // 童限年数
+ $this->assertEquals(6, $childLimit->getYearCount());
+ // 童限月数
+ $this->assertEquals(11, $childLimit->getMonthCount());
+ // 童限日数
+ $this->assertEquals(23, $childLimit->getDayCount());
+ // 童限结束(即开始起运)的公历时刻
+ $this->assertEquals('1997年3月11日 00:22:00', $childLimit->getEndTime()->__toString());
+
+ // 小运
+ $fortune = $childLimit->getStartFortune();
+ // 年龄
+ $this->assertEquals(7, $fortune->getAge());
+ }
+
+ function test17()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('己丑'),
+ SixtyCycle::fromName('戊辰'),
+ SixtyCycle::fromName('戊辰'),
+ SixtyCycle::fromName('甲子')
+ );
+ $this->assertEquals('丁丑', $eightChar->getOwnSign()->getName());
+ }
+
+ function test18()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('戊戌'),
+ SixtyCycle::fromName('庚申'),
+ SixtyCycle::fromName('丁亥'),
+ SixtyCycle::fromName('丙午')
+ );
+ $this->assertEquals('乙卯', $eightChar->getOwnSign()->getName());
+ }
+
+ function test19()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('甲子'),
+ SixtyCycle::fromName('壬申'),
+ SixtyCycle::fromName('庚子'),
+ SixtyCycle::fromName('乙亥')
+ );
+ $this->assertEquals('甲戌', $eightChar->getOwnSign()->getName());
+ }
+
+ function test20()
+ {
+ $eightChar = ChildLimit::fromSolarTime(SolarTime::fromYmdHms(2024, 1, 29, 9, 33, 0), Gender::MAN)->getEightChar();
+ $this->assertEquals('癸亥', $eightChar->getOwnSign()->getName());
+ $this->assertEquals('己未', $eightChar->getBodySign()->getName());
+ }
+
+ function test21()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('辛亥'),
+ SixtyCycle::fromName('乙未'),
+ SixtyCycle::fromName('庚子'),
+ SixtyCycle::fromName('甲辰')
+ );
+ $this->assertEquals('庚子', $eightChar->getBodySign()->getName());
+ }
+
+ function test22()
+ {
+ $this->assertEquals('丙寅', ChildLimit::fromSolarTime(SolarTime::fromYmdHms(1990, 1, 27, 0, 0, 0), Gender::MAN)->getEightChar()->getBodySign()->getName());
+ }
+
+ function test23()
+ {
+ $this->assertEquals('甲戌', ChildLimit::fromSolarTime(SolarTime::fromYmdHms(2019, 3, 7, 8, 0, 0), Gender::MAN)->getEightChar()->getOwnSign()->getName());
+ }
+
+ function test24()
+ {
+ $this->assertEquals('丁丑', ChildLimit::fromSolarTime(SolarTime::fromYmdHms(2019, 3, 27, 2, 0, 0), Gender::MAN)->getEightChar()->getOwnSign()->getName());
+ }
+
+ function test25()
+ {
+ $this->assertEquals('丙寅', LunarHour::fromYmdHms(1994, 5, 20, 18, 0, 0)->getEightChar()->getOwnSign()->getName());
+ }
+
+ function test26()
+ {
+ $this->assertEquals('己丑', SolarTime::fromYmdHms(1986, 5, 29, 13, 37, 0)->getLunarHour()->getEightChar()->getBodySign()->getName());
+ }
+
+ function test27()
+ {
+ $this->assertEquals('乙丑', SolarTime::fromYmdHms(1994, 12, 6, 2, 0, 0)->getLunarHour()->getEightChar()->getBodySign()->getName());
+ }
+
+ function test28()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('辛亥'),
+ SixtyCycle::fromName('丁酉'),
+ SixtyCycle::fromName('丙午'),
+ SixtyCycle::fromName('癸巳')
+ );
+ $this->assertEquals('辛卯', $eightChar->getOwnSign()->getName());
+ }
+
+ function test29()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('丙寅'),
+ SixtyCycle::fromName('庚寅'),
+ SixtyCycle::fromName('辛卯'),
+ SixtyCycle::fromName('壬辰')
+ );
+ $this->assertEquals('己亥', $eightChar->getOwnSign()->getName());
+ $this->assertEquals('乙未', $eightChar->getBodySign()->getName());
+ }
+
+ function test30()
+ {
+ $eightChar = new EightChar(
+ SixtyCycle::fromName('壬子'),
+ SixtyCycle::fromName('辛亥'),
+ SixtyCycle::fromName('壬戌'),
+ SixtyCycle::fromName('乙巳')
+ );
+ $this->assertEquals('乙巳', $eightChar->getBodySign()->getName());
+ }
+}
diff --git a/test/ElementTest.php b/test/ElementTest.php
new file mode 100644
index 0000000..c674be9
--- /dev/null
+++ b/test/ElementTest.php
@@ -0,0 +1,29 @@
+assertEquals('碓磨厕 外东南', SolarDay::fromYmd(2021, 11, 13)->getLunarDay()->getFetusDay()->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('占门碓 外东南', SolarDay::fromYmd(2021, 11, 12)->getLunarDay()->getFetusDay()->getName());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('厨灶厕 外西南', SolarDay::fromYmd(2011, 11, 12)->getLunarDay()->getFetusDay()->getName());
+ }
+}
diff --git a/test/FetusTest.php b/test/FetusTest.php
new file mode 100644
index 0000000..5068295
--- /dev/null
+++ b/test/FetusTest.php
@@ -0,0 +1,21 @@
+assertEquals('子', EarthBranch::fromIndex(0)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals(0, EarthBranch::fromName('子')->getIndex());
+ }
+}
diff --git a/test/HeavenlyStemTest.php b/test/HeavenlyStemTest.php
new file mode 100644
index 0000000..a12e0ef
--- /dev/null
+++ b/test/HeavenlyStemTest.php
@@ -0,0 +1,46 @@
+assertEquals('甲', HeavenStem::fromIndex(0)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals(0, HeavenStem::fromName('甲')->getIndex());
+ }
+
+ /**
+ * 天干的五行生克
+ */
+ function test2()
+ {
+ $this->assertEquals(HeavenStem::fromName('丙')->getElement(), HeavenStem::fromName('甲')->getElement()->getReinforce());
+ }
+
+ /**
+ * 十神
+ */
+ function test3()
+ {
+ $this->assertEquals('比肩', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('甲'))->getName());
+ $this->assertEquals('劫财', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('乙'))->getName());
+ $this->assertEquals('食神', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('丙'))->getName());
+ $this->assertEquals('伤官', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('丁'))->getName());
+ $this->assertEquals('偏财', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('戊'))->getName());
+ $this->assertEquals('正财', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('己'))->getName());
+ $this->assertEquals('七杀', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('庚'))->getName());
+ $this->assertEquals('正官', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('辛'))->getName());
+ $this->assertEquals('偏印', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('壬'))->getName());
+ $this->assertEquals('正印', HeavenStem::fromName('甲')->getTenStar(HeavenStem::fromName('癸'))->getName());
+ }
+}
diff --git a/test/JulianDayTest.php b/test/JulianDayTest.php
new file mode 100644
index 0000000..6b67b3d
--- /dev/null
+++ b/test/JulianDayTest.php
@@ -0,0 +1,16 @@
+assertEquals('2023年1月1日', SolarDay::fromYmd(2023, 1, 1)->getJulianDay()->getSolarDay()->__toString());
+ }
+}
diff --git a/test/LegalHolidayTest.php b/test/LegalHolidayTest.php
new file mode 100644
index 0000000..5974d85
--- /dev/null
+++ b/test/LegalHolidayTest.php
@@ -0,0 +1,54 @@
+assertNotNull($d);
+ $this->assertEquals('2011年5月1日 劳动节(休)', $d->__toString());
+
+ $this->assertEquals('2011年5月2日 劳动节(休)', $d->next(1)->__toString());
+ $this->assertEquals('2011年6月4日 端午节(休)', $d->next(2)->__toString());
+ $this->assertEquals('2011年4月30日 劳动节(休)', $d->next(-1)->__toString());
+ $this->assertEquals('2011年4月5日 清明节(休)', $d->next(-2)->__toString());
+ }
+
+
+ function test1()
+ {
+ $this->assertNotNull(LegalHoliday::fromYmd(2010, 1, 1));
+ }
+
+ function test3()
+ {
+ $d = LegalHoliday::fromYmd(2001, 12, 29);
+ $this->assertNotNull($d);
+ $this->assertEquals('2001年12月29日 元旦节(班)', $d->__toString());
+ $this->assertNull($d->next(-1));
+ }
+
+ function test4()
+ {
+ $d = LegalHoliday::fromYmd(2022, 10, 5);
+ $this->assertNotNull($d);
+ $this->assertEquals('2022年10月5日 国庆节(休)', $d->__toString());
+ $this->assertEquals('2022年10月4日 国庆节(休)', $d->next(-1)->__toString());
+ $this->assertEquals('2022年10月6日 国庆节(休)', $d->next(1)->__toString());
+ }
+
+ function test5()
+ {
+ $d = SolarDay::fromYmd(2010, 10, 1)->getLegalHoliday();
+ $this->assertNotNull($d);
+ $this->assertEquals('2010年10月1日 国庆节(休)', $d->__toString());
+ }
+}
diff --git a/test/LunarDayTest.php b/test/LunarDayTest.php
new file mode 100644
index 0000000..90667ad
--- /dev/null
+++ b/test/LunarDayTest.php
@@ -0,0 +1,166 @@
+assertEquals('1年1月1日', LunarDay::fromYmd(0, 11, 18)->getSolarDay()->__toString());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('9999年12月31日', LunarDay::fromYmd(9999, 12, 2)->getSolarDay()->__toString());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('1905年2月4日', LunarDay::fromYmd(1905, 1, 1)->getSolarDay()->__toString());
+ }
+
+ function test4()
+ {
+ $this->assertEquals('2039年1月23日', LunarDay::fromYmd(2038, 12, 29)->getSolarDay()->__toString());
+ }
+
+ function test5()
+ {
+ $this->assertEquals('1500年1月31日', LunarDay::fromYmd(1500, 1, 1)->getSolarDay()->__toString());
+ }
+
+ function test6()
+ {
+ $this->assertEquals('1501年1月18日', LunarDay::fromYmd(1500, 12, 29)->getSolarDay()->__toString());
+ }
+
+ function test7()
+ {
+ $this->assertEquals('1582年10月4日', LunarDay::fromYmd(1582, 9, 18)->getSolarDay()->__toString());
+ }
+
+ function test8()
+ {
+ $this->assertEquals('1582年10月15日', LunarDay::fromYmd(1582, 9, 19)->getSolarDay()->__toString());
+ }
+
+ function test9()
+ {
+ $this->assertEquals('2020年1月6日', LunarDay::fromYmd(2019, 12, 12)->getSolarDay()->__toString());
+ }
+
+ function test10()
+ {
+ $this->assertEquals('2033年12月22日', LunarDay::fromYmd(2033, -11, 1)->getSolarDay()->__toString());
+ }
+
+ function test11()
+ {
+ $this->assertEquals('2021年7月16日', LunarDay::fromYmd(2021, 6, 7)->getSolarDay()->__toString());
+ }
+
+ function test12()
+ {
+ $this->assertEquals('2034年2月19日', LunarDay::fromYmd(2034, 1, 1)->getSolarDay()->__toString());
+ }
+
+ function test13()
+ {
+ $this->assertEquals('2034年1月20日', LunarDay::fromYmd(2033, 12, 1)->getSolarDay()->__toString());
+ }
+
+ function test14()
+ {
+ $this->assertEquals('7013年12月24日', LunarDay::fromYmd(7013, -11, 4)->getSolarDay()->__toString());
+ }
+
+ function test15()
+ {
+ $this->assertEquals('己亥', LunarDay::fromYmd(2023, 8, 24)->getSixtyCycle()->__toString());
+ }
+
+ function test16()
+ {
+ $this->assertEquals('癸酉', LunarDay::fromYmd(1653, 1, 6)->getSixtyCycle()->__toString());
+ }
+
+ function test17()
+ {
+ $this->assertEquals('农历庚寅年二月初二', LunarDay::fromYmd(2010, 1, 1)->next(31)->__toString());
+ }
+
+ function test18()
+ {
+ $this->assertEquals('农历壬辰年闰四月初一', LunarDay::fromYmd(2012, 3, 1)->next(60)->__toString());
+ }
+
+ function test19()
+ {
+ $this->assertEquals('农历壬辰年闰四月廿九', LunarDay::fromYmd(2012, 3, 1)->next(88)->__toString());
+ }
+
+ function test20()
+ {
+ $this->assertEquals('农历壬辰年五月初一', LunarDay::fromYmd(2012, 3, 1)->next(89)->__toString());
+ }
+
+ function test21()
+ {
+ $this->assertEquals('2020年4月23日', LunarDay::fromYmd(2020, 4, 1)->getSolarDay()->__toString());
+ }
+
+ function test22()
+ {
+ $this->assertEquals('甲辰', LunarDay::fromYmd(2024, 1, 1)->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+
+ function test23()
+ {
+ $this->assertEquals('癸卯', LunarDay::fromYmd(2023, 12, 30)->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+
+ /**
+ * 二十八宿
+ */
+ function test24()
+ {
+ $d = LunarDay::fromYmd(2020, 4, 13);
+ $star = $d->getTwentyEightStar();
+ $this->assertEquals('南', $star->getZone()->getName());
+ $this->assertEquals('朱雀', $star->getZone()->getBeast()->getName());
+ $this->assertEquals('翼', $star->getName());
+ $this->assertEquals('火', $star->getSevenStar()->getName());
+ $this->assertEquals('蛇', $star->getAnimal()->getName());
+ $this->assertEquals('凶', $star->getLuck()->getName());
+
+ $this->assertEquals('阳天', $star->getLand()->getName());
+ $this->assertEquals('东南', $star->getLand()->getDirection()->getName());
+ }
+
+ function test25()
+ {
+ $d = LunarDay::fromYmd(2023, 9, 28);
+ $star = $d->getTwentyEightStar();
+ $this->assertEquals('南', $star->getZone()->getName());
+ $this->assertEquals('朱雀', $star->getZone()->getBeast()->getName());
+ $this->assertEquals('柳', $star->getName());
+ $this->assertEquals('土', $star->getSevenStar()->getName());
+ $this->assertEquals('獐', $star->getAnimal()->getName());
+ $this->assertEquals('凶', $star->getLuck()->getName());
+
+ $this->assertEquals('炎天', $star->getLand()->getName());
+ $this->assertEquals('南', $star->getLand()->getDirection()->getName());
+ }
+
+ function test26()
+ {
+ $lunar = LunarDay::fromYmd(2005, 11, 23);
+ $this->assertEquals('戊子', $lunar->getMonth()->getSixtyCycle()->getName());
+ $this->assertEquals('戊子', $lunar->getMonthSixtyCycle()->getName());
+ }
+}
diff --git a/test/LunarFestivalTest.php b/test/LunarFestivalTest.php
new file mode 100644
index 0000000..4c628eb
--- /dev/null
+++ b/test/LunarFestivalTest.php
@@ -0,0 +1,59 @@
+assertNotNull($f);
+ $this->assertEquals(LunarFestival::$NAMES[$i], $f->getName());
+ }
+ }
+
+ function test1()
+ {
+ $f = LunarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ for ($i = 0, $j = count(LunarFestival::$NAMES); $i < $j; $i++) {
+ $this->assertEquals(LunarFestival::$NAMES[$i], $f->next($i)->getName());
+ }
+ }
+
+ function test2()
+ {
+ $f = LunarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ $this->assertEquals('农历甲辰年正月初一 春节', $f->next(13)->__toString());
+ $this->assertEquals('农历壬寅年十一月廿九 冬至节', $f->next(-3)->__toString());
+ }
+
+ function test3()
+ {
+ $f = LunarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ $this->assertEquals('农历壬寅年三月初五 清明节', $f->next(-9)->__toString());
+ }
+
+ function test4()
+ {
+ $f = LunarDay::fromYmd(2010, 1, 15)->getFestival();
+ $this->assertNotNull($f);
+ $this->assertEquals('农历庚寅年正月十五 元宵节', $f->__toString());
+ }
+
+ function test5()
+ {
+ $f = LunarDay::fromYmd(2021, 12, 29)->getFestival();
+ $this->assertNotNull($f);
+ $this->assertEquals('农历辛丑年十二月廿九 除夕', $f->__toString());
+ }
+}
diff --git a/test/LunarHourTest.php b/test/LunarHourTest.php
new file mode 100644
index 0000000..80fe8d9
--- /dev/null
+++ b/test/LunarHourTest.php
@@ -0,0 +1,96 @@
+assertEquals('子时', $h->getName());
+ $this->assertEquals('农历庚子年闰四月初五戊子时', $h->__toString());
+ }
+
+ function test2()
+ {
+ $h = LunarHour::fromYmdHms(2020, -4, 5, 0, 59, 0);
+ $this->assertEquals('子时', $h->getName());
+ $this->assertEquals('农历庚子年闰四月初五丙子时', $h->__toString());
+ }
+
+ function test3()
+ {
+ $h = LunarHour::fromYmdHms(2020, -4, 5, 1, 0, 0);
+ $this->assertEquals('丑时', $h->getName());
+ $this->assertEquals('农历庚子年闰四月初五丁丑时', $h->__toString());
+ }
+
+ function test4()
+ {
+ $h = LunarHour::fromYmdHms(2020, -4, 5, 21, 30, 0);
+ $this->assertEquals('亥时', $h->getName());
+ $this->assertEquals('农历庚子年闰四月初五丁亥时', $h->__toString());
+ }
+
+ function test5()
+ {
+ $h = LunarHour::fromYmdHms(2020, -4, 2, 23, 30, 0);
+ $this->assertEquals('子时', $h->getName());
+ $this->assertEquals('农历庚子年闰四月初二壬子时', $h->__toString());
+ }
+
+ function test6()
+ {
+ $h = LunarHour::fromYmdHms(2020, 4, 28, 23, 30, 0);
+ $this->assertEquals('子时', $h->getName());
+ $this->assertEquals('农历庚子年四月廿八甲子时', $h->__toString());
+ }
+
+ function test7()
+ {
+ $h = LunarHour::fromYmdHms(2020, 4, 29, 0, 0, 0);
+ $this->assertEquals('子时', $h->getName());
+ $this->assertEquals('农历庚子年四月廿九甲子时', $h->__toString());
+ }
+
+ function test8()
+ {
+ $h = LunarHour::fromYmdHms(2023, 11, 14, 23, 0, 0);
+ $this->assertEquals('甲子', $h->getSixtyCycle()->getName());
+
+ $this->assertEquals('己未', $h->getDaySixtyCycle()->getName());
+ $this->assertEquals('戊午', $h->getDay()->getSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年十一月十四', $h->getDay()->__toString());
+
+ $this->assertEquals('甲子', $h->getMonthSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年十一月', $h->getDay()->getMonth()->__toString());
+ $this->assertEquals('乙丑', $h->getDay()->getMonth()->getSixtyCycle()->getName());
+
+ $this->assertEquals('癸卯', $h->getYearSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年', $h->getDay()->getMonth()->getYear()->__toString());
+ $this->assertEquals('癸卯', $h->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+
+ function test9()
+ {
+ $h = LunarHour::fromYmdHms(2023, 11, 14, 6, 0, 0);
+ $this->assertEquals('乙卯', $h->getSixtyCycle()->getName());
+
+ $this->assertEquals('戊午', $h->getDaySixtyCycle()->getName());
+ $this->assertEquals('戊午', $h->getDay()->getSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年十一月十四', $h->getDay()->__toString());
+
+ $this->assertEquals('甲子', $h->getMonthSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年十一月', $h->getDay()->getMonth()->__toString());
+ $this->assertEquals('乙丑', $h->getDay()->getMonth()->getSixtyCycle()->getName());
+
+ $this->assertEquals('癸卯', $h->getYearSixtyCycle()->getName());
+ $this->assertEquals('农历癸卯年', $h->getDay()->getMonth()->getYear()->__toString());
+ $this->assertEquals('癸卯', $h->getDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+}
diff --git a/test/LunarMonthTest.php b/test/LunarMonthTest.php
new file mode 100644
index 0000000..b0caf1e
--- /dev/null
+++ b/test/LunarMonthTest.php
@@ -0,0 +1,258 @@
+assertEquals('七月', LunarMonth::fromYm(2359, 7)->getName());
+ }
+
+ /**
+ * 闰月
+ */
+
+ function test1()
+ {
+ $this->assertEquals('闰七月', LunarMonth::fromYm(2359, -7)->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals(29, LunarMonth::fromYm(2023, 6)->getDayCount());
+ }
+
+ function test3()
+ {
+ $this->assertEquals(30, LunarMonth::fromYm(2023, 7)->getDayCount());
+ }
+
+ function test4()
+ {
+ $this->assertEquals(30, LunarMonth::fromYm(2023, 8)->getDayCount());
+ }
+
+ function test5()
+ {
+ $this->assertEquals(29, LunarMonth::fromYm(2023, 9)->getDayCount());
+ }
+
+ function test6()
+ {
+ $this->assertEquals('2023年10月15日', LunarMonth::fromYm(2023, 9)->getFirstJulianDay()->getSolarDay()->__toString());
+ }
+
+ function test7()
+ {
+ $this->assertEquals('甲寅', LunarMonth::fromYm(2023, 1)->getSixtyCycle()->getName());
+ }
+
+ function test8()
+ {
+ $this->assertEquals('丙辰', LunarMonth::fromYm(2023, -2)->getSixtyCycle()->getName());
+ }
+
+ function test9()
+ {
+ $this->assertEquals('丁巳', LunarMonth::fromYm(2023, 3)->getSixtyCycle()->getName());
+ }
+
+ function test10()
+ {
+ $this->assertEquals('丙寅', LunarMonth::fromYm(2024, 1)->getSixtyCycle()->getName());
+ }
+
+ function test11()
+ {
+ $this->assertEquals('丙寅', LunarMonth::fromYm(2023, 12)->getSixtyCycle()->getName());
+ }
+
+ function test12()
+ {
+ $this->assertEquals('壬寅', LunarMonth::fromYm(2022, 1)->getSixtyCycle()->getName());
+ }
+
+ function test13()
+ {
+ $this->assertEquals('闰十二月', LunarMonth::fromYm(37, -12)->getName());
+ }
+
+ function test14()
+ {
+ $this->assertEquals('闰十二月', LunarMonth::fromYm(5552, -12)->getName());
+ }
+
+ function test15()
+ {
+ $this->assertEquals('农历戊子年十二月', LunarMonth::fromYm(2008, 11)->next(1)->__toString());
+ }
+
+ function test16()
+ {
+ $this->assertEquals('农历己丑年正月', LunarMonth::fromYm(2008, 11)->next(2)->__toString());
+ }
+
+ function test17()
+ {
+ $this->assertEquals('农历己丑年五月', LunarMonth::fromYm(2008, 11)->next(6)->__toString());
+ }
+
+ function test18()
+ {
+ $this->assertEquals('农历己丑年闰五月', LunarMonth::fromYm(2008, 11)->next(7)->__toString());
+ }
+
+ function test19()
+ {
+ $this->assertEquals('农历己丑年六月', LunarMonth::fromYm(2008, 11)->next(8)->__toString());
+ }
+
+ function test20()
+ {
+ $this->assertEquals('农历庚寅年正月', LunarMonth::fromYm(2008, 11)->next(15)->__toString());
+ }
+
+ function test21()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2008, 12)->next(-1)->__toString());
+ }
+
+ function test22()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2009, 1)->next(-2)->__toString());
+ }
+
+ function test23()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2009, 5)->next(-6)->__toString());
+ }
+
+ function test24()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2009, -5)->next(-7)->__toString());
+ }
+
+ function test25()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2009, 6)->next(-8)->__toString());
+ }
+
+ function test26()
+ {
+ $this->assertEquals('农历戊子年十一月', LunarMonth::fromYm(2010, 1)->next(-15)->__toString());
+ }
+
+ function test27()
+ {
+ $this->assertEquals(29, LunarMonth::fromYm(2012, -4)->getDayCount());
+ }
+
+ function test28()
+ {
+ $this->assertEquals('癸亥', LunarMonth::fromYm(2023, 9)->getSixtyCycle()->__toString());
+ }
+
+ function test29()
+ {
+ $d = SolarDay::fromYmd(2023, 10, 7)->getLunarDay();
+ $this->assertEquals('壬戌', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('辛酉', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test30()
+ {
+ $d = SolarDay::fromYmd(2023, 10, 8)->getLunarDay();
+ $this->assertEquals('壬戌', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('壬戌', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test31()
+ {
+ $d = SolarDay::fromYmd(2023, 10, 15)->getLunarDay();
+ $this->assertEquals('九月', $d->getMonth()->getName());
+ $this->assertEquals('癸亥', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('壬戌', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test32()
+ {
+ $d = SolarDay::fromYmd(2023, 11, 7)->getLunarDay();
+ $this->assertEquals('癸亥', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('壬戌', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test33()
+ {
+ $d = SolarDay::fromYmd(2023, 11, 8)->getLunarDay();
+ $this->assertEquals('癸亥', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('癸亥', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test34()
+ {
+ // 2023年闰2月
+ $m = LunarMonth::fromYm(2023, 12);
+ $this->assertEquals('农历癸卯年十二月', $m->__toString());
+ $this->assertEquals('农历癸卯年十一月', $m->next(-1)->__toString());
+ $this->assertEquals('农历癸卯年十月', $m->next(-2)->__toString());
+ }
+
+ function test35()
+ {
+ // 2023年闰2月
+ $m = LunarMonth::fromYm(2023, 3);
+ $this->assertEquals('农历癸卯年三月', $m->__toString());
+ $this->assertEquals('农历癸卯年闰二月', $m->next(-1)->__toString());
+ $this->assertEquals('农历癸卯年二月', $m->next(-2)->__toString());
+ $this->assertEquals('农历癸卯年正月', $m->next(-3)->__toString());
+ $this->assertEquals('农历壬寅年十二月', $m->next(-4)->__toString());
+ $this->assertEquals('农历壬寅年十一月', $m->next(-5)->__toString());
+ }
+
+ function test36()
+ {
+ $d = SolarDay::fromYmd(1983, 2, 15)->getLunarDay();
+ $this->assertEquals('甲寅', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('甲寅', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test37()
+ {
+ $d = SolarDay::fromYmd(2023, 10, 30)->getLunarDay();
+ $this->assertEquals('癸亥', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('壬戌', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test38()
+ {
+ $d = SolarDay::fromYmd(2023, 10, 19)->getLunarDay();
+ $this->assertEquals('癸亥', $d->getMonth()->getSixtyCycle()->__toString());
+ $this->assertEquals('壬戌', $d->getMonthSixtyCycle()->__toString());
+ }
+
+ function test39()
+ {
+ $m = LunarMonth::fromYm(2023, 11);
+ $this->assertEquals('农历癸卯年十一月', $m->__toString());
+ $this->assertEquals('乙丑', $m->getSixtyCycle()->__toString());
+ }
+
+ function test40()
+ {
+ $this->assertEquals('庚申', LunarDay::fromYmd(2018, 6, 26)->getMonthSixtyCycle()->__toString());
+ }
+
+ function test41()
+ {
+ $this->assertEquals('辛丑', LunarMonth::fromYm(1991, 12)->getSixtyCycle()->__toString());
+ }
+
+}
diff --git a/test/LunarYearTest.php b/test/LunarYearTest.php
new file mode 100644
index 0000000..671d7d7
--- /dev/null
+++ b/test/LunarYearTest.php
@@ -0,0 +1,138 @@
+assertEquals('农历癸卯年', LunarYear::fromYear(2023)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('农历戊申年', LunarYear::fromYear(2023)->next(5)->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('农历戊戌年', LunarYear::fromYear(2023)->next(-5)->getName());
+ }
+
+ /**
+ * 农历年的干支
+ */
+ function test3()
+ {
+ $this->assertEquals('庚子', LunarYear::fromYear(2020)->getSixtyCycle()->getName());
+ }
+
+ /**
+ * 农历年的生肖(农历年->干支->地支->生肖)
+ */
+ function test4()
+ {
+ $this->assertEquals('虎', LunarYear::fromYear(1986)->getSixtyCycle()->getEarthBranch()->getZodiac()->getName());
+ }
+
+ function test5()
+ {
+ $this->assertEquals(12, LunarYear::fromYear(151)->getLeapMonth());
+ }
+
+ function test6()
+ {
+ $this->assertEquals(1, LunarYear::fromYear(2357)->getLeapMonth());
+ }
+
+ function test7()
+ {
+ $y = LunarYear::fromYear(2023);
+ $this->assertEquals('癸卯', $y->getSixtyCycle()->getName());
+ $this->assertEquals('兔', $y->getSixtyCycle()->getEarthBranch()->getZodiac()->getName());
+ }
+
+ function test8()
+ {
+ $this->assertEquals('上元', LunarYear::fromYear(1864)->getTwenty()->getSixty()->getName());
+ }
+
+ function test9()
+ {
+ $this->assertEquals('上元', LunarYear::fromYear(1923)->getTwenty()->getSixty()->getName());
+ }
+
+ function test10()
+ {
+ $this->assertEquals('中元', LunarYear::fromYear(1924)->getTwenty()->getSixty()->getName());
+ }
+
+ function test11()
+ {
+ $this->assertEquals('中元', LunarYear::fromYear(1983)->getTwenty()->getSixty()->getName());
+ }
+
+ function test12()
+ {
+ $this->assertEquals('下元', LunarYear::fromYear(1984)->getTwenty()->getSixty()->getName());
+ }
+
+ function test13()
+ {
+ $this->assertEquals('下元', LunarYear::fromYear(2043)->getTwenty()->getSixty()->getName());
+ }
+
+ function test14()
+ {
+ $this->assertEquals('一运', LunarYear::fromYear(1864)->getTwenty()->getName());
+ }
+
+ function test15()
+ {
+ $this->assertEquals('一运', LunarYear::fromYear(1883)->getTwenty()->getName());
+ }
+
+ function test16()
+ {
+ $this->assertEquals('二运', LunarYear::fromYear(1884)->getTwenty()->getName());
+ }
+
+ function test17()
+ {
+ $this->assertEquals('二运', LunarYear::fromYear(1903)->getTwenty()->getName());
+ }
+
+ function test18()
+ {
+ $this->assertEquals('三运', LunarYear::fromYear(1904)->getTwenty()->getName());
+ }
+
+ function test19()
+ {
+ $this->assertEquals('三运', LunarYear::fromYear(1923)->getTwenty()->getName());
+ }
+
+ function test20()
+ {
+ $this->assertEquals('八运', LunarYear::fromYear(2004)->getTwenty()->getName());
+ }
+
+ function test21()
+ {
+ $year = LunarYear::fromYear(1);
+ $this->assertEquals('六运', $year->getTwenty()->getName());
+ $this->assertEquals('中元', $year->getTwenty()->getSixty()->getName());
+ }
+
+ function test22()
+ {
+ $year = LunarYear::fromYear(1863);
+ $this->assertEquals('九运', $year->getTwenty()->getName());
+ $this->assertEquals('下元', $year->getTwenty()->getSixty()->getName());
+ }
+}
diff --git a/test/NineDayTest.php b/test/NineDayTest.php
new file mode 100644
index 0000000..7b9d8c3
--- /dev/null
+++ b/test/NineDayTest.php
@@ -0,0 +1,65 @@
+getNineDay();
+ $this->assertEquals('一九', $d->getName());
+ $this->assertEquals('一九', $d->getNine()->__toString());
+ $this->assertEquals('一九第1天', $d->__toString());
+ }
+
+ function test1()
+ {
+ $d = SolarDay::fromYmd(2020, 12, 22)->getNineDay();
+ $this->assertEquals('一九', $d->getName());
+ $this->assertEquals('一九', $d->getNine()->__toString());
+ $this->assertEquals('一九第2天', $d->__toString());
+ }
+
+ function test2()
+ {
+ $d = SolarDay::fromYmd(2020, 1, 7)->getNineDay();
+ $this->assertEquals('二九', $d->getName());
+ $this->assertEquals('二九', $d->getNine()->__toString());
+ $this->assertEquals('二九第8天', $d->__toString());
+ }
+
+ function test3()
+ {
+ $d = SolarDay::fromYmd(2021, 1, 6)->getNineDay();
+ $this->assertEquals('二九', $d->getName());
+ $this->assertEquals('二九', $d->getNine()->__toString());
+ $this->assertEquals('二九第8天', $d->__toString());
+ }
+
+ function test4()
+ {
+ $d = SolarDay::fromYmd(2021, 1, 8)->getNineDay();
+ $this->assertEquals('三九', $d->getName());
+ $this->assertEquals('三九', $d->getNine()->__toString());
+ $this->assertEquals('三九第1天', $d->__toString());
+ }
+
+ function test5()
+ {
+ $d = SolarDay::fromYmd(2021, 3, 5)->getNineDay();
+ $this->assertEquals('九九', $d->getName());
+ $this->assertEquals('九九', $d->getNine()->__toString());
+ $this->assertEquals('九九第3天', $d->__toString());
+ }
+
+ function test6()
+ {
+ $d = SolarDay::fromYmd(2021, 7, 5)->getNineDay();
+ $this->assertNull($d);
+ }
+}
diff --git a/test/NineStarTest.php b/test/NineStarTest.php
new file mode 100644
index 0000000..93a11b8
--- /dev/null
+++ b/test/NineStarTest.php
@@ -0,0 +1,99 @@
+getNineStar();
+ $this->assertEquals('六', $nineStar->getName());
+ $this->assertEquals('六白金', $nineStar->__toString());
+ }
+
+ function test1()
+ {
+ $nineStar = LunarYear::fromYear(2022)->getNineStar();
+ $this->assertEquals('五黄土', $nineStar->__toString());
+ $this->assertEquals('玉衡', $nineStar->getDipper()->__toString());
+ }
+
+ function test2()
+ {
+ $nineStar = LunarYear::fromYear(2033)->getNineStar();
+ $this->assertEquals('三碧木', $nineStar->__toString());
+ $this->assertEquals('天玑', $nineStar->getDipper()->__toString());
+ }
+
+ function test3()
+ {
+ $nineStar = LunarMonth::fromYm(1985, 2)->getNineStar();
+ $this->assertEquals('四绿木', $nineStar->__toString());
+ $this->assertEquals('天权', $nineStar->getDipper()->__toString());
+ }
+
+ function test4()
+ {
+ $nineStar = LunarMonth::fromYm(1985, 2)->getNineStar();
+ $this->assertEquals('四绿木', $nineStar->__toString());
+ $this->assertEquals('天权', $nineStar->getDipper()->__toString());
+ }
+
+ function test5()
+ {
+ $nineStar = LunarMonth::fromYm(2022, 1)->getNineStar();
+ $this->assertEquals('二黒土', $nineStar->__toString());
+ $this->assertEquals('天璇', $nineStar->getDipper()->__toString());
+ }
+
+ function test6()
+ {
+ $nineStar = LunarMonth::fromYm(2033, 1)->getNineStar();
+ $this->assertEquals('五黄土', $nineStar->__toString());
+ $this->assertEquals('玉衡', $nineStar->getDipper()->__toString());
+ }
+
+ function test7()
+ {
+ $nineStar = SolarDay::fromYmd(1985, 2, 19)->getLunarDay()->getNineStar();
+ $this->assertEquals('五黄土', $nineStar->__toString());
+ $this->assertEquals('玉衡', $nineStar->getDipper()->__toString());
+ }
+
+ function test8()
+ {
+ $nineStar = LunarDay::fromYmd(2022, 1, 1)->getNineStar();
+ $this->assertEquals('四绿木', $nineStar->__toString());
+ $this->assertEquals('天权', $nineStar->getDipper()->__toString());
+ }
+
+ function test9()
+ {
+ $nineStar = LunarDay::fromYmd(2033, 1, 1)->getNineStar();
+ $this->assertEquals('一白水', $nineStar->__toString());
+ $this->assertEquals('天枢', $nineStar->getDipper()->__toString());
+ }
+
+ function test10()
+ {
+ $nineStar = LunarHour::fromYmdHms(2033, 1, 1, 12, 0, 0)->getNineStar();
+ $this->assertEquals('七赤金', $nineStar->__toString());
+ $this->assertEquals('摇光', $nineStar->getDipper()->__toString());
+ }
+
+ function test11()
+ {
+ $nineStar = LunarHour::fromYmdHms(2011, 5, 3, 23, 0, 0)->getNineStar();
+ $this->assertEquals('七赤金', $nineStar->__toString());
+ $this->assertEquals('摇光', $nineStar->getDipper()->__toString());
+ }
+}
diff --git a/test/PhenologyTest.php b/test/PhenologyTest.php
new file mode 100644
index 0000000..440314a
--- /dev/null
+++ b/test/PhenologyTest.php
@@ -0,0 +1,39 @@
+getPhenologyDay();
+ // 三候
+ $threePhenology = $phenology->getPhenology()->getThreePhenology();
+ $this->assertEquals('谷雨', $solarDay->getTerm()->getName());
+ $this->assertEquals('初候', $threePhenology->getName());
+ $this->assertEquals('萍始生', $phenology->getName());
+ // 该候的第5天
+ $this->assertEquals(4, $phenology->getDayIndex());
+ }
+
+ function test1()
+ {
+ $solarDay = SolarDay::fromYmd(2021, 12, 26);
+ // 七十二候
+ $phenology = $solarDay->getPhenologyDay();
+ // 三候
+ $threePhenology = $phenology->getPhenology()->getThreePhenology();
+ $this->assertEquals('冬至', $solarDay->getTerm()->getName());
+ $this->assertEquals('二候', $threePhenology->getName());
+ $this->assertEquals('麋角解', $phenology->getName());
+ // 该候的第1天
+ $this->assertEquals(0, $phenology->getDayIndex());
+ }
+}
diff --git a/test/SixtyCycleTest.php b/test/SixtyCycleTest.php
new file mode 100644
index 0000000..746bfa4
--- /dev/null
+++ b/test/SixtyCycleTest.php
@@ -0,0 +1,62 @@
+assertEquals('丁丑', SixtyCycle::fromIndex(13)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals(13, SixtyCycle::fromName('丁丑')->getIndex());
+ }
+
+ /**
+ * 五行
+ */
+ function test2()
+ {
+ $this->assertEquals('石榴木', SixtyCycle::fromName('辛酉')->getSound()->getName());
+ $this->assertEquals('剑锋金', SixtyCycle::fromName('癸酉')->getSound()->getName());
+ $this->assertEquals('平地木', SixtyCycle::fromName('己亥')->getSound()->getName());
+ }
+
+ /**
+ * 旬
+ */
+ function test3()
+ {
+ $this->assertEquals('甲子', SixtyCycle::fromName('甲子')->getTen()->getName());
+ $this->assertEquals('甲寅', SixtyCycle::fromName('乙卯')->getTen()->getName());
+ $this->assertEquals('甲申', SixtyCycle::fromName('癸巳')->getTen()->getName());
+ }
+
+ /**
+ * 旬空
+ */
+ function test4()
+ {
+ $this->assertEquals([EarthBranch::fromName('戌'), EarthBranch::fromName('亥')], SixtyCycle::fromName('甲子')->getExtraEarthBranches());
+ $this->assertEquals([EarthBranch::fromName('子'), EarthBranch::fromName('丑')], SixtyCycle::fromName('乙卯')->getExtraEarthBranches());
+ $this->assertEquals([EarthBranch::fromName('午'), EarthBranch::fromName('未')], SixtyCycle::fromName('癸巳')->getExtraEarthBranches());
+ }
+
+ /**
+ * 地势(长生十二神)
+ */
+ function test5()
+ {
+ $this->assertEquals('长生', HeavenStem::fromName('丙')->getTerrain(EarthBranch::fromName('寅'))->getName());
+ $this->assertEquals('沐浴', HeavenStem::fromName('辛')->getTerrain(EarthBranch::fromName('亥'))->getName());
+ }
+}
diff --git a/test/SolarDayTest.php b/test/SolarDayTest.php
new file mode 100644
index 0000000..dfb6c58
--- /dev/null
+++ b/test/SolarDayTest.php
@@ -0,0 +1,92 @@
+assertEquals('1日', SolarDay::fromYmd(2023, 1, 1)->getName());
+ $this->assertEquals('2023年1月1日', SolarDay::fromYmd(2023, 1, 1)->__toString());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('29日', SolarDay::fromYmd(2000, 2, 29)->getName());
+ $this->assertEquals('2000年2月29日', SolarDay::fromYmd(2000, 2, 29)->__toString());
+ }
+
+ function test2()
+ {
+ $this->assertEquals(0, SolarDay::fromYmd(2023, 1, 1)->getIndexInYear());
+ $this->assertEquals(364, SolarDay::fromYmd(2023, 12, 31)->getIndexInYear());
+ $this->assertEquals(365, SolarDay::fromYmd(2020, 12, 31)->getIndexInYear());
+ }
+
+ function test3()
+ {
+ $this->assertEquals(0, SolarDay::fromYmd(2023, 1, 1)->subtract(SolarDay::fromYmd(2023, 1, 1)));
+ $this->assertEquals(1, SolarDay::fromYmd(2023, 1, 2)->subtract(SolarDay::fromYmd(2023, 1, 1)));
+ $this->assertEquals(-1, SolarDay::fromYmd(2023, 1, 1)->subtract(SolarDay::fromYmd(2023, 1, 2)));
+ $this->assertEquals(31, SolarDay::fromYmd(2023, 2, 1)->subtract(SolarDay::fromYmd(2023, 1, 1)));
+ $this->assertEquals(-31, SolarDay::fromYmd(2023, 1, 1)->subtract(SolarDay::fromYmd(2023, 2, 1)));
+ $this->assertEquals(365, SolarDay::fromYmd(2024, 1, 1)->subtract(SolarDay::fromYmd(2023, 1, 1)));
+ $this->assertEquals(-365, SolarDay::fromYmd(2023, 1, 1)->subtract(SolarDay::fromYmd(2024, 1, 1)));
+ $this->assertEquals(1, SolarDay::fromYmd(1582, 10, 15)->subtract(SolarDay::fromYmd(1582, 10, 4)));
+ }
+
+ function test4()
+ {
+ $this->assertEquals('1582年10月4日', SolarDay::fromYmd(1582, 10, 15)->next(-1)->__toString());
+ }
+
+ function test5()
+ {
+ $this->assertEquals('2000年3月1日', SolarDay::fromYmd(2000, 2, 28)->next(2)->__toString());
+ }
+
+ function test6()
+ {
+ $this->assertEquals('农历庚子年闰四月初二', SolarDay::fromYmd(2020, 5, 24)->getLunarDay()->__toString());
+ }
+
+ function test7()
+ {
+ $this->assertEquals(31, SolarDay::fromYmd(2020, 5, 24)->subtract(SolarDay::fromYmd(2020, 4, 23)));
+ }
+
+ function test8()
+ {
+ $this->assertEquals('农历丙子年十一月十二', SolarDay::fromYmd(16, 11, 30)->getLunarDay()->__toString());
+ }
+
+ function test9()
+ {
+ $this->assertEquals('霜降', SolarDay::fromYmd(2023, 10, 27)->getTerm()->__toString());
+ }
+
+ function test10()
+ {
+ $this->assertEquals('豺乃祭兽第4天', SolarDay::fromYmd(2023, 10, 27)->getPhenologyDay()->__toString());
+ }
+
+ function test11()
+ {
+ $this->assertEquals('初候', SolarDay::fromYmd(2023, 10, 27)->getPhenologyDay()->getPhenology()->getThreePhenology()->__toString());
+ }
+
+ function test22()
+ {
+ $this->assertEquals('甲辰', SolarDay::fromYmd(2024, 2, 10)->getLunarDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+
+ function test23()
+ {
+ $this->assertEquals('癸卯', SolarDay::fromYmd(2024, 2, 9)->getLunarDay()->getMonth()->getYear()->getSixtyCycle()->getName());
+ }
+}
diff --git a/test/SolarFestivalTest.php b/test/SolarFestivalTest.php
new file mode 100644
index 0000000..09a35cb
--- /dev/null
+++ b/test/SolarFestivalTest.php
@@ -0,0 +1,65 @@
+assertNotNull($f);
+ $this->assertEquals(SolarFestival::$NAMES[$i], $f->getName());
+ }
+ }
+
+ function test1()
+ {
+ $f = SolarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ for ($i = 0, $j = count(SolarFestival::$NAMES); $i < $j; $i++) {
+ $this->assertEquals(SolarFestival::$NAMES[$i], $f->next($i)->getName());
+ }
+ }
+
+ function test2()
+ {
+ $f = SolarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ $this->assertEquals('2024年5月1日 五一劳动节', $f->next(13)->__toString());
+ $this->assertEquals('2022年8月1日 八一建军节', $f->next(-3)->__toString());
+ }
+
+ function test3()
+ {
+ $f = SolarFestival::fromIndex(2023, 0);
+ $this->assertNotNull($f);
+ $this->assertEquals('2022年3月8日 三八妇女节', $f->next(-9)->__toString());
+ }
+
+ function test4()
+ {
+ $f = SolarDay::fromYmd(2010, 1, 1)->getFestival();
+ $this->assertNotNull($f);
+ $this->assertEquals('2010年1月1日 元旦', $f->__toString());
+ }
+
+ function test5()
+ {
+ $f = SolarDay::fromYmd(2021, 5, 4)->getFestival();
+ $this->assertNotNull($f);
+ $this->assertEquals('2021年5月4日 五四青年节', $f->__toString());
+ }
+
+ function test6()
+ {
+ $f = SolarDay::fromYmd(1939, 5, 4)->getFestival();
+ $this->assertNull($f);
+ }
+}
diff --git a/test/SolarHalfYearTest.php b/test/SolarHalfYearTest.php
new file mode 100644
index 0000000..62df3cd
--- /dev/null
+++ b/test/SolarHalfYearTest.php
@@ -0,0 +1,41 @@
+assertEquals('上半年', SolarHalfYear::fromIndex(2023, 0)->getName());
+ $this->assertEquals('2023年上半年', SolarHalfYear::fromIndex(2023, 0)->__toString());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('下半年', SolarHalfYear::fromIndex(2023, 1)->getName());
+ $this->assertEquals('2023年下半年', SolarHalfYear::fromIndex(2023, 1)->__toString());
+ }
+
+ function test2()
+ {
+ $this->assertEquals('下半年', SolarHalfYear::fromIndex(2023, 0)->next(1)->getName());
+ $this->assertEquals('2023年下半年', SolarHalfYear::fromIndex(2023, 0)->next(1)->__toString());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('上半年', SolarHalfYear::fromIndex(2023, 0)->next(2)->getName());
+ $this->assertEquals('2024年上半年', SolarHalfYear::fromIndex(2023, 0)->next(2)->__toString());
+ }
+
+ function test4()
+ {
+ $this->assertEquals('上半年', SolarHalfYear::fromIndex(2023, 0)->next(-2)->getName());
+ $this->assertEquals('2022年上半年', SolarHalfYear::fromIndex(2023, 0)->next(-2)->__toString());
+ }
+}
diff --git a/test/SolarMonthTest.php b/test/SolarMonthTest.php
new file mode 100644
index 0000000..3ce8629
--- /dev/null
+++ b/test/SolarMonthTest.php
@@ -0,0 +1,61 @@
+assertEquals('5月', $m->getName());
+ $this->assertEquals('2019年5月', $m->__toString());
+ }
+
+ function test1()
+ {
+ $m = SolarMonth::fromYm(2023, 1);
+ $this->assertEquals(5, $m->getWeekCount(0));
+ $this->assertEquals(6, $m->getWeekCount(1));
+ $this->assertEquals(6, $m->getWeekCount(2));
+ $this->assertEquals(5, $m->getWeekCount(3));
+ $this->assertEquals(5, $m->getWeekCount(4));
+ $this->assertEquals(5, $m->getWeekCount(5));
+ $this->assertEquals(5, $m->getWeekCount(6));
+ }
+
+ function test2()
+ {
+ $m = SolarMonth::fromYm(2023, 2);
+ $this->assertEquals(5, $m->getWeekCount(0));
+ $this->assertEquals(5, $m->getWeekCount(1));
+ $this->assertEquals(5, $m->getWeekCount(2));
+ $this->assertEquals(4, $m->getWeekCount(3));
+ $this->assertEquals(5, $m->getWeekCount(4));
+ $this->assertEquals(5, $m->getWeekCount(5));
+ $this->assertEquals(5, $m->getWeekCount(6));
+ }
+
+ function test3()
+ {
+ $m = SolarMonth::fromYm(2023, 10)->next(1);
+ $this->assertEquals('11月', $m->getName());
+ $this->assertEquals('2023年11月', $m->__toString());
+ }
+
+ function test4()
+ {
+ $m = SolarMonth::fromYm(2023, 10);
+ $this->assertEquals('2023年12月', $m->next(2)->__toString());
+ $this->assertEquals('2024年1月', $m->next(3)->__toString());
+ $this->assertEquals('2023年5月', $m->next(-5)->__toString());
+ $this->assertEquals('2023年1月', $m->next(-9)->__toString());
+ $this->assertEquals('2022年12月', $m->next(-10)->__toString());
+ $this->assertEquals('2025年10月', $m->next(24)->__toString());
+ $this->assertEquals('2021年10月', $m->next(-24)->__toString());
+ }
+}
diff --git a/test/SolarTermTest.php b/test/SolarTermTest.php
new file mode 100644
index 0000000..16a79f7
--- /dev/null
+++ b/test/SolarTermTest.php
@@ -0,0 +1,69 @@
+assertEquals('冬至', $dongZhi->getName());
+ $this->assertEquals(0, $dongZhi->getIndex());
+ // 儒略日
+ // 公历日
+ $this->assertEquals('2022年12月22日', $dongZhi->getJulianDay()->getSolarDay()->__toString());
+
+ // 冬至顺推23次,就是大雪 2023-12-07 17:32:55
+ $daXue = $dongZhi->next(23);
+ $this->assertEquals('大雪', $daXue->getName());
+ $this->assertEquals(23, $daXue->getIndex());
+ $this->assertEquals('2023年12月7日', $daXue->getJulianDay()->getSolarDay()->__toString());
+
+ // 冬至逆推2次,就是上一年的小雪 2022-11-22 16:20:28
+ $xiaoXue = $dongZhi->next(-2);
+ $this->assertEquals('小雪', $xiaoXue->getName());
+ $this->assertEquals(22, $xiaoXue->getIndex());
+ $this->assertEquals('2022年11月22日', $xiaoXue->getJulianDay()->getSolarDay()->__toString());
+
+ // 冬至顺推24次,就是下一个冬至 2023-12-22 11:27:20
+ $dongZhi2 = $dongZhi->next(24);
+ $this->assertEquals('冬至', $dongZhi2->getName());
+ $this->assertEquals(0, $dongZhi2->getIndex());
+ $this->assertEquals('2023年12月22日', $dongZhi2->getJulianDay()->getSolarDay()->__toString());
+ }
+
+ function test1()
+ {
+ // 公历2023年的雨水,2023-02-19 06:34:16
+ $jq = SolarTerm::fromName(2023, '雨水');
+ $this->assertEquals('雨水', $jq->getName());
+ $this->assertEquals(4, $jq->getIndex());
+ }
+
+ function test2()
+ {
+ // 公历2023年的大雪,2023-12-07 17:32:55
+ $jq = SolarTerm::fromName(2023, '大雪');
+ $this->assertEquals('大雪', $jq->getName());
+ // 索引
+ $this->assertEquals(23, $jq->getIndex());
+ // 公历
+ $this->assertEquals('2023年12月7日', $jq->getJulianDay()->getSolarDay()->__toString());
+ // 农历
+ $this->assertEquals('农历癸卯年十月廿五', $jq->getJulianDay()->getSolarDay()->getLunarDay()->__toString());
+ // 推移
+ $this->assertEquals('雨水', $jq->next(5)->getName());
+ }
+
+ function test3()
+ {
+ $this->assertEquals('寒露', SolarDay::fromYmd(2023, 10, 10)->getTerm()->getName());
+ }
+}
diff --git a/test/SolarYearTest.php b/test/SolarYearTest.php
new file mode 100644
index 0000000..9205594
--- /dev/null
+++ b/test/SolarYearTest.php
@@ -0,0 +1,46 @@
+assertEquals('2023年', SolarYear::fromYear(2023)->getName());
+ }
+
+ function test1()
+ {
+ $this->assertFalse(SolarYear::fromYear(2023)->isLeap());
+ }
+
+ function test2()
+ {
+ $this->assertTrue(SolarYear::fromYear(1500)->isLeap());
+ }
+
+ function test3()
+ {
+ $this->assertFalse(SolarYear::fromYear(1700)->isLeap());
+ }
+
+ function test4()
+ {
+ $this->assertEquals(365, SolarYear::fromYear(2023)->getDayCount());
+ }
+
+ function test5()
+ {
+ $this->assertEquals('2028年', SolarYear::fromYear(2023)->next(5)->getName());
+ }
+
+ function test6()
+ {
+ $this->assertEquals('2018年', SolarYear::fromYear(2023)->next(-5)->getName());
+ }
+}
diff --git a/test/WeekTest.php b/test/WeekTest.php
new file mode 100644
index 0000000..0e60553
--- /dev/null
+++ b/test/WeekTest.php
@@ -0,0 +1,152 @@
+assertEquals('一', SolarDay::fromYmd(1582, 10, 1)->getWeek()->getName());
+ }
+
+ function test1()
+ {
+ $this->assertEquals('五', SolarDay::fromYmd(1582, 10, 15)->getWeek()->getName());
+ }
+
+ function test2()
+ {
+ $this->assertEquals(2, SolarDay::fromYmd(2023, 10, 31)->getWeek()->getIndex());
+ }
+
+ function test3()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0);
+ $this->assertEquals('第一周', $w->getName());
+ $this->assertEquals('2023年10月第一周', $w->__toString());
+ }
+
+ function test5()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 4, 0);
+ $this->assertEquals('第五周', $w->getName());
+ $this->assertEquals('2023年10月第五周', $w->__toString());
+ }
+
+ function test6()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 5, 1);
+ $this->assertEquals('第六周', $w->getName());
+ $this->assertEquals('2023年10月第六周', $w->__toString());
+ }
+
+ function test7()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0)->next(4);
+ $this->assertEquals('第五周', $w->getName());
+ $this->assertEquals('2023年10月第五周', $w->__toString());
+ }
+
+ function test8()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0)->next(5);
+ $this->assertEquals('第二周', $w->getName());
+ $this->assertEquals('2023年11月第二周', $w->__toString());
+ }
+
+ function test9()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0)->next(-1);
+ $this->assertEquals('第五周', $w->getName());
+ $this->assertEquals('2023年9月第五周', $w->__toString());
+ }
+
+ function test10()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0)->next(-5);
+ $this->assertEquals('第一周', $w->getName());
+ $this->assertEquals('2023年9月第一周', $w->__toString());
+ }
+
+ function test11()
+ {
+ $w = SolarWeek::fromYm(2023, 10, 0, 0)->next(-6);
+ $this->assertEquals('第四周', $w->getName());
+ $this->assertEquals('2023年8月第四周', $w->__toString());
+ }
+
+ function test12()
+ {
+ $solar = SolarDay::fromYmd(1582, 10, 1);
+ $this->assertEquals(1, $solar->getWeek()->getIndex());
+ }
+
+ function test13()
+ {
+ $solar = SolarDay::fromYmd(1582, 10, 15);
+ $this->assertEquals(5, $solar->getWeek()->getIndex());
+ }
+
+ function test14()
+ {
+ $solar = SolarDay::fromYmd(1129, 11, 17);
+ $this->assertEquals(0, $solar->getWeek()->getIndex());
+ }
+
+ function test15()
+ {
+ $solar = SolarDay::fromYmd(1129, 11, 1);
+ $this->assertEquals(5, $solar->getWeek()->getIndex());
+ }
+
+ function test16()
+ {
+ $solar = SolarDay::fromYmd(8, 11, 1);
+ $this->assertEquals(4, $solar->getWeek()->getIndex());
+ }
+
+ function test17()
+ {
+ $solar = SolarDay::fromYmd(1582, 9, 30);
+ $this->assertEquals(0, $solar->getWeek()->getIndex());
+ }
+
+ function test18()
+ {
+ $solar = SolarDay::fromYmd(1582, 1, 1);
+ $this->assertEquals(1, $solar->getWeek()->getIndex());
+ }
+
+ function test19()
+ {
+ $solar = SolarDay::fromYmd(1500, 2, 29);
+ $this->assertEquals(6, $solar->getWeek()->getIndex());
+ }
+
+ function test20()
+ {
+ $solar = SolarDay::fromYmd(9865, 7, 26);
+ $this->assertEquals(3, $solar->getWeek()->getIndex());
+ }
+
+ function test21()
+ {
+ $week = LunarWeek::fromYm(2023, 1, 0, 2);
+ $this->assertEquals('农历癸卯年正月第一周', $week->__toString());
+ $this->assertEquals('农历壬寅年十二月廿六', $week->getFirstDay()->__toString());
+ }
+
+ function test22()
+ {
+ $week = SolarWeek::fromYm(2023, 1, 0, 2);
+ $this->assertEquals('2023年1月第一周', $week->__toString());
+ $this->assertEquals('2022年12月27日', $week->getFirstDay()->__toString());
+ }
+}
diff --git a/tools/build-standalone.php b/tools/build-standalone.php
new file mode 100644
index 0000000..b0212a5
--- /dev/null
+++ b/tools/build-standalone.php
@@ -0,0 +1,120 @@
+path = $path;
+ $classInfo->namespaces = $namespaces;
+ $classInfo->uses = $uses;
+ return $classInfo;
+}
+
+function parseDirectory($path): void
+{
+ global $out;
+ $namespaces = array();
+ $uses = array();
+
+ $files = glob(rtrim($path, '/') . '/*');
+ if ('../src' == $path) {
+ usort($files, function ($a, $b) {
+ $sorts = array();
+ foreach (['Culture', 'Tyme', 'AbstractCulture', 'AbstractCultureDay', 'AbstractTyme', 'LoopTyme'] as $name) {
+ $sorts[] = '../src/'. $name . '.php';
+ }
+ return array_search($a, $sorts) - array_search($b, $sorts);
+ });
+ }
+ foreach ($files as $file) {
+ if (is_file($file)) {
+ $classInfo = parseFile($file);
+ foreach ($classInfo->namespaces as $line) {
+ if (!in_array($line, $namespaces)) {
+ $namespaces[] = $line;
+ }
+ }
+ foreach ($classInfo->uses as $line) {
+ if (!in_array($line, $uses)) {
+ $uses[] = $line;
+ }
+ }
+ }
+ }
+
+ if (count($namespaces) > 0) {
+ foreach ($namespaces as $line) {
+ fwrite($out, $line);
+ }
+ fwrite($out, "\r\n\r\n");
+ }
+
+ if (count($uses) > 0) {
+ foreach ($uses as $line) {
+ fwrite($out, $line);
+ }
+ fwrite($out, "\r\n");
+ }
+
+ foreach ($files as $file) {
+ if (is_file($file)) {
+ writeClass($file);
+ }
+ }
+
+ foreach ($files as $file) {
+ if (is_dir($file)) {
+ parseDirectory($file);
+ }
+ }
+}
+
+parseDirectory('../src');
+
+echo('Done!');