Issues (31)

src/Calendar.php (6 issues)

1
<?php
2
/*
3
 * This file is part of the tinymeng/tools.
4
 * (c) overtrue <[email protected]>
5
 * This source file is subject to the MIT license that is bundled
6
 * with this source code in the file LICENSE.
7
 */
8
namespace tinymeng\tools;
9
10
use DateTime;
11
use DateTimeZone;
12
use InvalidArgumentException;
13
14
/**
15
 * Class Calendar.
16
 *
17
 * @author overtrue <[email protected]>
18
 */
19
class Calendar
20
{
21
    /**
22
     * 农历 1900-2100 的润大小信息.
23
     *
24
     * @var array
25
     */
26
    protected $lunars = [
27
        0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909
28
        0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919
29
        0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929
30
        0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939
31
        0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949
32
        0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959
33
        0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969
34
        0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979
35
        0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989
36
        0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999
37
        0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009
38
        0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019
39
        0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029
40
        0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039
41
        0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049
42
        0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059
43
        0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069
44
        0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079
45
        0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089
46
        0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099
47
        0x0d520, // 2100
48
    ];
49
50
    /**
51
     * 公历每个月份的天数表.
52
     *
53
     * @var array
54
     */
55
    protected $solarMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
56
57
    /**
58
     * 天干地支之天干速查表.
59
     *
60
     * @var array
61
     */
62
    protected $gan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
63
64
    /**
65
     * 天干地支之天干速查表 <=> 色彩.
66
     *
67
     * @var array
68
     */
69
    protected $colors = ['青', '青', '红', '红', '黄', '黄', '白', '白', '黑', '黑'];
70
71
    /**
72
     * 天干地支之天干速查表 <=> 五行.
73
     *
74
     * @var array
75
     */
76
    protected $wuXing = ['木', '木', '火', '火', '土', '土', '金', '金', '水', '水'];
77
78
    /**
79
     * 地支 <=> 五行.
80
     *
81
     * @var array
82
     */
83
    protected $zhiWuxing = ['水', '土', '木', '木', '土', '火', '火', '土', '金', '金', '土', '水'];
84
85
    /**
86
     * 天干地支之地支速查表.
87
     *
88
     * @var array
89
     */
90
    protected $zhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
91
92
    /**
93
     * 天干地支之地支速查表 <=> 生肖.
94
     *
95
     * @var array
96
     */
97
    protected $animals = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
98
99
    /**
100
     * 24节气速查表.
101
     *
102
     * @var array
103
     */
104
    protected $solarTerm = [
105
        '小寒', '大寒', '立春', '雨水', '惊蛰', '春分',
106
        '清明', '谷雨', '立夏', '小满', '芒种', '夏至',
107
        '小暑', '大暑', '立秋', '处暑', '白露', '秋分',
108
        '寒露', '霜降', '立冬', '小雪', '大雪', '冬至',
109
    ];
110
111
    /**
112
     * 1900-2100 各年的 24 节气日期速查表.
113
     *
114
     * @var array
115
     */
116
    protected $solarTerms = [
117
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
118
        '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
119
        '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
120
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
121
        'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
122
        '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
123
        '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
124
        '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
125
        '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
126
        '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
127
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
128
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
129
        '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
130
        '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
131
        '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
132
        '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
133
        '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
134
        '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
135
        '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
136
        '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
137
        '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
138
        '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
139
        '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
140
        '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
141
        '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
142
        '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
143
        '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
144
        '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
145
        '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
146
        '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
147
        '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
148
        '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
149
        '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
150
        '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
151
        '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
152
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
153
        '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
154
        '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
155
        '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
156
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
157
        '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
158
        '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
159
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
160
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
161
        '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
162
        '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
163
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
164
        '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
165
        '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
166
        '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
167
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
168
        '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
169
        '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
170
        '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
171
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
172
        '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
173
        '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
174
        '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
175
        '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
176
        '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
177
        '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
178
        '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
179
        '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
180
        '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
181
        '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
182
        '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
183
        '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
184
    ];
185
186
    /**
187
     * 数字转中文速查表.
188
     *
189
     * @var array
190
     */
191
    protected $weekdayAlias = ['日', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
192
193
    /**
194
     * 日期转农历称呼速查表.
195
     *
196
     * @var array
197
     */
198
    protected $dateAlias = ['初', '十', '廿', '卅'];
199
200
    /**
201
     * 月份转农历称呼速查表.
202
     *
203
     * @var array
204
     */
205
    protected $monthAlias = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
206
207
    /**
208
     * 传入阳历年月日获得详细的公历、农历信息.
209
     *
210
     * @param int $year
211
     * @param int $month
212
     * @param int $day
213
     * @param int $hour
214
     *
215
     * @return array
216
     */
217
    public function solar($year, $month, $day, $hour = null)
218
    {
219
        $date = $this->makeDate("{$year}-{$month}-{$day}");
220
        $lunar = $this->solar2lunar($year, $month, $day, $hour);
221
        $week = abs((int)$date->format('w')); // 0 ~ 6 修正 星期七 为 星期日
222
223
        return array_merge(
224
            $lunar,
225
            [
226
                'gregorian_year' => (string) $year,
227
                'gregorian_month' => sprintf('%02d', $month),
228
                'gregorian_day' => sprintf('%02d', $day),
229
                'gregorian_hour' => !is_numeric($hour) || $hour < 0 || $hour > 23 ? null : sprintf('%02d', $hour),
230
                'week_no' => $week, // 在周日时将会传回 0
231
                'week_name' => '星期'.$this->weekdayAlias[$week],
232
                'is_today' => 0 === $this->makeDate('now')->diff($date)->days,
233
                'constellation' => $this->toConstellation($month, $day),
234
                'is_same_year' => $lunar['lunar_year'] == $year ?: false,
235
            ]
236
        );
237
    }
238
239
    /**
240
     * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历信息.
241
     *
242
     * @param int  $year        lunar year
243
     * @param int  $month       lunar month
244
     * @param int  $day         lunar day
245
     * @param bool $isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
246
     * @param int  $hour        birth hour.[0~23]
247
     *
248
     * @return array
249
     */
250
    public function lunar($year, $month, $day, $isLeapMonth = false, $hour = null)
251
    {
252
        $solar = $this->lunar2solar($year, $month, $day, $isLeapMonth);
253
254
        return $this->solar($solar['solar_year'], $solar['solar_month'], $solar['solar_day'], $hour);
255
    }
256
257
    /**
258
     * 返回农历指定年的总天数.
259
     *
260
     * @param int $year
261
     *
262
     * @return int
263
     */
264
    public function daysOfYear($year)
265
    {
266
        $sum = 348;
267
268
        for ($i = 0x8000; $i > 0x8; $i >>= 1) {
269
            $sum += ($this->lunars[$year - 1900] & $i) ? 1 : 0;
270
        }
271
272
        return $sum + $this->leapDays($year);
273
    }
274
275
    /**
276
     * 返回农历指定年的总月数.
277
     *
278
     * @param int $year
279
     *
280
     * @return int
281
     */
282
    public function monthsOfYear($year)
283
    {
284
        return 0 < $this->leapMonth($year) ? 13 : 12;
285
    }
286
287
    /**
288
     * 返回农历 y 年闰月是哪个月;若 y 年没有闰月 则返回0.
289
     *
290
     * @param int $year
291
     *
292
     * @return int
293
     */
294
    public function leapMonth($year)
295
    {
296
        // 闰字编码 \u95f0
297
        return $this->lunars[$year - 1900] & 0xf;
298
    }
299
300
    /**
301
     * 返回农历y年闰月的天数 若该年没有闰月则返回 0.
302
     *
303
     * @param int $year
304
     *
305
     * @return int
306
     */
307
    public function leapDays($year)
308
    {
309
        if ($this->leapMonth($year)) {
310
            return ($this->lunars[$year - 1900] & 0x10000) ? 30 : 29;
311
        }
312
313
        return 0;
314
    }
315
316
    /**
317
     * 返回农历 y 年 m 月(非闰月)的总天数,计算 m 为闰月时的天数请使用 leapDays 方法.
318
     *
319
     * @param int $year
320
     * @param int $month
321
     *
322
     * @return int
323
     */
324
    public function lunarDays($year, $month)
325
    {
326
        // 月份参数从 1 至 12,参数错误返回 -1
327
        if ($month > 12 || $month < 1) {
328
            return -1;
329
        }
330
331
        return ($this->lunars[$year - 1900] & (0x10000 >> $month)) ? 30 : 29;
332
    }
333
334
    /**
335
     * 返回公历 y 年 m 月的天数.
336
     *
337
     * @param int $year
338
     * @param int $month
339
     *
340
     * @return int
341
     */
342
    public function solarDays($year, $month)
343
    {
344
        // 若参数错误 返回-1
345
        if ($month > 12 || $month < 1) {
346
            return -1;
347
        }
348
349
        $ms = $month - 1;
350
351
        if (1 == $ms) { // 2 月份的闰平规律测算后确认返回 28 或 29
352
            return ((0 === $year % 4) && (0 !== $year % 100) || (0 === $year % 400)) ? 29 : 28;
353
        }
354
355
        return $this->solarMonth[$ms];
356
    }
357
358
    /**
359
     * 农历年份转换为干支纪年.
360
     *
361
     * @param int      $lunarYear
362
     * @param null|int $termIndex
363
     *
364
     * @return string
365
     */
366
    public function ganZhiYear($lunarYear, $termIndex = null)
367
    {
368
        /**
369
         * 据维基百科干支词条:『在西历新年后,华夏新年或干支历新年之前,则续用上一年之干支』
370
         * 所以干支年份应该不需要根据节气校正,为免影响现有系统,此处暂时保留原有逻辑
371
         * https://zh.wikipedia.org/wiki/%E5%B9%B2%E6%94%AF.
372
         *
373
         * 即使考虑节气,有的年份没有立春,有的年份有两个立春,此处逻辑仍不能处理该特殊情况
374
         */
375
        $adjust = null !== $termIndex && 3 > $termIndex ? 1 : 0;
376
377
        $ganKey = ($lunarYear + $adjust - 4) % 10;
378
        $zhiKey = ($lunarYear + $adjust - 4) % 12;
379
380
        return $this->gan[$ganKey].$this->zhi[$zhiKey];
381
    }
382
383
    /**
384
     * 公历月、日判断所属星座.
385
     *
386
     * @param int $gregorianMonth
387
     * @param int $gregorianDay
388
     *
389
     * @return string
390
     */
391
    public function toConstellation($gregorianMonth, $gregorianDay)
392
    {
393
        $constellations = '魔羯水瓶双鱼白羊金牛双子巨蟹狮子处女天秤天蝎射手魔羯';
394
        $arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
395
396
        return mb_substr(
397
            $constellations,
398
            $gregorianMonth * 2 - ($gregorianDay < $arr[$gregorianMonth - 1] ? 2 : 0),
399
            2,
400
            'UTF-8'
401
        );
402
    }
403
404
    /**
405
     * 传入offset偏移量返回干支.
406
     *
407
     * @param int $offset 相对甲子的偏移量
408
     *
409
     * @return string
410
     */
411
    public function toGanZhi($offset)
412
    {
413
        return $this->gan[$offset % 10].$this->zhi[$offset % 12];
414
    }
415
416
    /**
417
     * 传入公历年获得该年第n个节气的公历日期
418
     *
419
     * @param int $year 公历年(1900-2100);
420
     * @param int $no   二十四节气中的第几个节气(1~24);从n=1(小寒)算起
421
     *
422
     * @return int
423
     *
424
     * @example
425
     * <pre>
426
     *  $_24 = $this->getTerm(1987,3) ;// _24 = 4; 意即 1987 年 2 月 4 日立春
427
     * </pre>
428
     */
429
    public function getTerm($year, $no)
430
    {
431
        if ($year < 1900 || $year > 2100) {
432
            return -1;
433
        }
434
        if ($no < 1 || $no > 24) {
435
            return -1;
436
        }
437
        $solarTermsOfYear = array_map('hexdec', (array)str_split($this->solarTerms[$year - 1900], 5));
438
        $positions = [
439
            0 => [0, 1],
440
            1 => [1, 2],
441
            2 => [3, 1],
442
            3 => [4, 2],
443
        ];
444
        $group = intval(($no - 1) / 4);
445
        list($offset, $length) = $positions[($no - 1) % 4];
446
447
        return substr($solarTermsOfYear[$group], $offset, $length);
448
    }
449
450
    public function toChinaYear($year)
451
    {
452
        if (!is_numeric($year)) {
453
            throw new InvalidArgumentException("错误的年份:{$year}");
454
        }
455
        $lunarYear = '';
456
        $year = (string) $year;
457
        for ($i = 0, $l = strlen($year); $i < $l; ++$i) {
458
            $lunarYear .= '0' !== $year[$i] ? $this->weekdayAlias[$year[$i]] : '零';
459
        }
460
461
        return $lunarYear;
462
    }
463
464
    /**
465
     * 传入农历数字月份返回汉语通俗表示法.
466
     *
467
     * @param int $month
468
     *
469
     * @return string
470
     */
471
    public function toChinaMonth($month)
472
    {
473
        // 若参数错误 返回 -1
474
        if ($month > 12 || $month < 1) {
475
            throw new InvalidArgumentException("错误的月份:{$month}");
476
        }
477
478
        return $this->monthAlias[abs($month) - 1].'月';
479
    }
480
481
    /**
482
     * 传入农历日期数字返回汉字表示法.
483
     *
484
     * @param int $day
485
     *
486
     * @return string
487
     */
488
    public function toChinaDay($day)
489
    {
490
        switch ($day) {
491
            case 10:
492
                return '初十';
493
            case 20:
494
                return '二十';
495
            case 30:
496
                return '三十';
497
            default:
498
                return $this->dateAlias[intval($day / 10)].$this->weekdayAlias[$day % 10];
499
        }
500
    }
501
502
    /**
503
     * 年份转生肖.
504
     *
505
     * 仅能大致转换, 精确划分生肖分界线是 “立春”.
506
     *
507
     * @param int      $year
508
     * @param null|int $termIndex
509
     *
510
     * @return string
511
     */
512
    public function getAnimal($year, $termIndex = null)
513
    {
514
        // 认为此逻辑不需要,详情参见 ganZhiYear 相关注释
515
        $adjust = null !== $termIndex && 3 > $termIndex ? 1 : 0;
516
517
        $animalIndex = ($year + $adjust - 4) % 12;
518
519
        return $this->animals[$animalIndex];
520
    }
521
522
    /**
523
     * 干支转色彩.
524
     *
525
     * @param $ganZhi
526
     *
527
     * @return string
528
     */
529
    protected function getColor($ganZhi)
530
    {
531
        if (!$ganZhi) {
532
            return null;
533
        }
534
535
        $gan = substr($ganZhi, 0, 3);
536
537
        if (!$gan) {
538
            return null;
539
        }
540
541
        return $this->colors[array_search($gan, $this->gan)];
542
    }
543
544
    /**
545
     * 干支转五行.
546
     *
547
     * @param $ganZhi
548
     *
549
     * @return string
550
     */
551
    protected function getWuXing($ganZhi)
552
    {
553
        if (!$ganZhi) {
554
            return null;
555
        }
556
557
        $gan = substr($ganZhi, 0, 3);
558
        $zhi = substr($ganZhi, 3);
559
560
        if (!$gan || !$zhi) {
561
            return null;
562
        }
563
564
        $wGan = $this->wuXing[array_search($gan, $this->gan)];
565
        $wZhi = $this->zhiWuxing[array_search($zhi, $this->zhi)];
566
567
        return $wGan.$wZhi;
568
    }
569
570
    /**
571
     * 阳历转阴历.
572
     *
573
     * @param int $year
574
     * @param int $month
575
     * @param int $day
576
     * @param int $hour
577
     *
578
     * @return array
579
     */
580
    public function solar2lunar($year, $month, $day, $hour = null)
581
    {
582
        if (23 == $hour) {
583
            // 23点过后算子时,农历以子时为一天的起始
584
            $date = $this->makeDate("{$year}-{$month}-{$day} +1day");
585
        } else {
586
            $date = $this->makeDate("{$year}-{$month}-{$day}");
587
        }
588
589
        list($year, $month, $day) = explode('-', $date->format('Y-n-j'));
590
591
        // 参数区间1900.1.31~2100.12.31
592
        if ($year < 1900 || $year > 2100) {
593
            throw new InvalidArgumentException("不支持的年份:{$year}");
594
        }
595
596
        // 年份限定、上限
597
        if (1900 == $year && 1 == $month && $day < 31) {
598
            throw new InvalidArgumentException("不支持的日期:{$year}-{$month}-{$day}");
599
        }
600
601
        $offset = (int)$this->dateDiff($date, '1900-01-31')->days;
602
603
        for ($i = 1900; $i < 2101 && $offset > 0; ++$i) {
604
            $daysOfYear = $this->daysOfYear($i);
605
            $offset -= $daysOfYear;
606
        }
607
608
        if ($offset < 0) {
609
            $offset += $daysOfYear;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $daysOfYear does not seem to be defined for all execution paths leading up to this point.
Loading history...
610
            --$i;
611
        }
612
613
        // 农历年
614
        $lunarYear = $i;
615
616
        $leap = $this->leapMonth($i); // 闰哪个月
617
        $isLeap = false;
618
619
        // 用当年的天数 offset,逐个减去每月(农历)的天数,求出当天是本月的第几天
620
        for ($i = 1; $i < 13 && $offset > 0; ++$i) {
621
            // 闰月
622
            if ($leap > 0 && $i == ($leap + 1) && !$isLeap) {
623
                --$i;
624
                $isLeap = true;
625
                $daysOfMonth = $this->leapDays($lunarYear); // 计算农历月天数
626
            } else {
627
                $daysOfMonth = $this->lunarDays($lunarYear, $i); // 计算农历普通月天数
628
            }
629
630
            // 解除闰月
631
            if (true === $isLeap && $i == ($leap + 1)) {
632
                $isLeap = false;
633
            }
634
635
            $offset -= $daysOfMonth;
636
        }
637
        // offset为0时,并且刚才计算的月份是闰月,要校正
638
        if (0 === $offset && $leap > 0 && $i == $leap + 1) {
639
            if ($isLeap) {
640
                $isLeap = false;
641
            } else {
642
                $isLeap = true;
643
                --$i;
644
            }
645
        }
646
647
        if ($offset < 0) {
648
            $offset += $daysOfMonth;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $daysOfMonth does not seem to be defined for all execution paths leading up to this point.
Loading history...
649
            --$i;
650
        }
651
652
        // 农历月
653
        $lunarMonth = $i;
654
655
        // 农历日
656
        $lunarDay = $offset + 1;
657
658
        // 月柱 1900 年 1 月小寒以前为 丙子月(60进制12)
659
        $firstNode = $this->getTerm($year, ($month * 2 - 1)); // 返回当月「节气」为几日开始
0 ignored issues
show
$year of type string is incompatible with the type integer expected by parameter $year of tinymeng\tools\Calendar::getTerm(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

659
        $firstNode = $this->getTerm(/** @scrutinizer ignore-type */ $year, ($month * 2 - 1)); // 返回当月「节气」为几日开始
Loading history...
660
        $secondNode = $this->getTerm($year, ($month * 2)); // 返回当月「节气」为几日开始
661
662
        // 依据 12 节气修正干支月
663
        $ganZhiMonth = $this->toGanZhi(($year - 1900) * 12 + $month + 11);
664
665
        if ($day >= $firstNode) {
666
            $ganZhiMonth = $this->toGanZhi(($year - 1900) * 12 + $month + 12);
667
        }
668
669
        // 获取该天的节气
670
        $termIndex = null;
671
        if ($firstNode == $day) {
672
            $termIndex = $month * 2 - 2;
673
        }
674
675
        if ($secondNode == $day) {
676
            $termIndex = $month * 2 - 1;
677
        }
678
679
        $term = null !== $termIndex ? $this->solarTerm[$termIndex] : null;
680
681
        // 日柱 当月一日与 1900/1/1 相差天数
682
        $dayCyclical = $this->dateDiff("{$year}-{$month}-01", '1900-01-01')->days + 10;
683
        $dayCyclical += $day - 1;
684
        $ganZhiDay = $this->toGanZhi($dayCyclical);
685
686
        // 时柱和时辰
687
        list($ganZhiHour, $lunarHour, $hour) = $this->ganZhiHour($hour, $dayCyclical);
688
689
        $ganZhiYear = $this->ganZhiYear($lunarYear, $termIndex);
690
691
        return [
692
            'lunar_year' => (string) $lunarYear,
693
            'lunar_month' => sprintf('%02d', $lunarMonth),
694
            'lunar_day' => sprintf('%02d', $lunarDay),
695
            'lunar_hour' => $hour,
696
            'lunar_year_chinese' => $this->toChinaYear($lunarYear),
697
            'lunar_month_chinese' => ($isLeap ? '闰' : '').$this->toChinaMonth($lunarMonth),
698
            'lunar_day_chinese' => $this->toChinaDay($lunarDay),
699
            'lunar_hour_chinese' => $lunarHour,
700
            'ganzhi_year' => $ganZhiYear,
701
            'ganzhi_month' => $ganZhiMonth,
702
            'ganzhi_day' => $ganZhiDay,
703
            'ganzhi_hour' => $ganZhiHour,
704
            'wuxing_year' => $this->getWuXing($ganZhiYear),
705
            'wuxing_month' => $this->getWuXing($ganZhiMonth),
706
            'wuxing_day' => $this->getWuXing($ganZhiDay),
707
            'wuxing_hour' => $this->getWuXing($ganZhiHour),
708
            'color_year' => $this->getColor($ganZhiYear),
709
            'color_month' => $this->getColor($ganZhiMonth),
710
            'color_day' => $this->getColor($ganZhiDay),
711
            'color_hour' => $this->getColor($ganZhiHour),
712
            'animal' => $this->getAnimal($lunarYear, $termIndex),
713
            'term' => $term,
714
            'is_leap' => $isLeap,
715
        ];
716
    }
717
718
    /**
719
     * 阴历转阳历.
720
     *
721
     * @param int  $year
722
     * @param int  $month
723
     * @param int  $day
724
     * @param bool $isLeapMonth
725
     *
726
     * @return array|int
727
     */
728
    public function lunar2solar($year, $month, $day, $isLeapMonth = false)
729
    {
730
        // 参数区间 1900.1.3 1 ~2100.12.1
731
        $leapMonth = $this->leapMonth($year);
732
733
        // 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
734
        if ($isLeapMonth && ($leapMonth != $month)) {
735
            $isLeapMonth = false;
736
        }
737
738
        // 超出了最大极限值
739
        if (2100 == $year && 12 == $month && $day > 1 || 1900 == $year && 1 == $month && $day < 31) {
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (2100 == $year && 12 == ... == $month && $day < 31, Probably Intended Meaning: 2100 == $year && 12 == $...== $month && $day < 31)
Loading history...
740
            return -1;
741
        }
742
743
        $maxDays = $days = $this->lunarDays($year, $month);
744
745
        // if month is leap, _day use leapDays method
746
        if ($isLeapMonth) {
747
            $maxDays = $this->leapDays($year, $month);
0 ignored issues
show
The call to tinymeng\tools\Calendar::leapDays() has too many arguments starting with $month. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

747
            /** @scrutinizer ignore-call */ 
748
            $maxDays = $this->leapDays($year, $month);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
748
        }
749
750
        // 参数合法性效验
751
        if ($year < 1900 || $year > 2100 || $day > $maxDays) {
752
            throw new InvalidArgumentException('传入的参数不合法');
753
        }
754
755
        // 计算农历的时间差
756
        $offset = 0;
757
758
        for ($i = 1900; $i < $year; ++$i) {
759
            $offset += $this->daysOfYear($i);
760
        }
761
762
        $isAdd = false;
763
        for ($i = 1; $i < $month; ++$i) {
764
            $leap = $this->leapMonth($year);
765
            if (!$isAdd) {// 处理闰月
766
                if ($leap <= $i && $leap > 0) {
767
                    $offset += $this->leapDays($year);
768
                    $isAdd = true;
769
                }
770
            }
771
            $offset += $this->lunarDays($year, $i);
772
        }
773
774
        // 转换闰月农历 需补充该年闰月的前一个月的时差
775
        if ($isLeapMonth) {
776
            $offset += $days;
777
        }
778
779
        // 1900 年农历正月一日的公历时间为 1900 年 1 月 30 日 0 时 0 分 0 秒 (该时间也是本农历的最开始起始点)
780
        // XXX: 部分 windows 机器不支持负时间戳,所以这里就写死了,哈哈哈哈...
781
        $startTimestamp = -2206483200;
782
        $date = date('Y-m-d', ($offset + $day) * 86400 + $startTimestamp);
783
784
        list($solarYear, $solarMonth, $solarDay) = explode('-', $date);
785
786
        return [
787
            'solar_year' => $solarYear,
788
            'solar_month' => sprintf('%02d', $solarMonth),
789
            'solar_day' => sprintf('%02d', $solarDay),
790
        ];
791
    }
792
793
    /**
794
     * 获取两个日期之间的距离.
795
     *
796
     * @param string|\DateTime $date1
797
     * @param string|\DateTime $date2
798
     *
799
     * @return bool|\DateInterval
800
     */
801
    public function dateDiff($date1, $date2)
802
    {
803
        if (!($date1 instanceof DateTime)) {
804
            $date1 = $this->makeDate($date1);
805
        }
806
807
        if (!($date2 instanceof DateTime)) {
808
            $date2 = $this->makeDate($date2);
809
        }
810
811
        return $date1->diff($date2);
812
    }
813
814
    /**
815
     * 获取两个日期之间以年为单位的距离.
816
     *
817
     * @param array $lunar1
818
     * @param array $lunar2
819
     * @param bool  $absolute
820
     *
821
     * @return int
822
     */
823
    public function diffInYears($lunar1, $lunar2, $absolute = true)
824
    {
825
        $solar1 =
826
            $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
827
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
828
829
        $solar2 =
830
            $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
831
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
832
833
        if ($date1 < $date2) {
834
            $lessLunar = $lunar1;
835
            $greaterLunar = $lunar2;
836
            $changed = false;
837
        } else {
838
            $lessLunar = $lunar2;
839
            $greaterLunar = $lunar1;
840
            $changed = true;
841
        }
842
843
        $monthAdjustFactor = $greaterLunar['lunar_day'] >= $lessLunar['lunar_day'] ? 0 : 1;
844
        if ($greaterLunar['lunar_month'] == $lessLunar['lunar_month']) {
845
            if ($greaterLunar['is_leap'] && !$lessLunar['is_leap']) {
846
                $monthAdjustFactor = 0;
847
            } elseif (!$greaterLunar['is_leap'] && $lessLunar['is_leap']) {
848
                $monthAdjustFactor = 1;
849
            }
850
        }
851
        $yearAdjustFactor = $greaterLunar['lunar_month'] - $monthAdjustFactor >= $lessLunar['lunar_month'] ? 0 : 1;
852
        $diff = $greaterLunar['lunar_year'] - $yearAdjustFactor - $lessLunar['lunar_year'];
853
854
        return $absolute ? $diff : ($changed ? -1 * $diff : $diff);
855
    }
856
857
    /**
858
     * 获取两个日期之间以月为单位的距离.
859
     * @param array $lunar1
860
     * @param array $lunar2
861
     * @param bool $absolute
862
     * @return float|int|mixed
863
     */
864
    public function diffInMonths($lunar1, $lunar2, $absolute = true)
865
    {
866
        $solar1 =
867
            $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
868
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
869
870
        $solar2 =
871
            $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
872
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
873
874
        if ($date1 < $date2) {
875
            $lessLunar = $lunar1;
876
            $greaterLunar = $lunar2;
877
            $changed = false;
878
        } else {
879
            $lessLunar = $lunar2;
880
            $greaterLunar = $lunar1;
881
            $changed = true;
882
        }
883
884
        $diff = 0;
885
886
        if ($lessLunar['lunar_year'] == $greaterLunar['lunar_year']) {
887
            $leapMonth = $this->leapMonth($lessLunar['lunar_year']);
888
            $lessLunarAdjustFactor =
889
                $lessLunar['is_leap'] || (0 < $leapMonth && $leapMonth < $lessLunar['lunar_month']) ? 1 : 0;
890
            $greaterLunarAdjustFactor =
891
                $greaterLunar['is_leap'] || (0 < $leapMonth && $leapMonth < $greaterLunar['lunar_month']) ? 1 : 0;
892
            $diff =
893
                $greaterLunar['lunar_month'] + $greaterLunarAdjustFactor - $lessLunar['lunar_month'] - $lessLunarAdjustFactor;
894
        } else {
895
            $lessLunarLeapMonth = $this->leapMonth($lessLunar['lunar_year']);
896
            $greaterLunarLeapMonth = $this->leapMonth($greaterLunar['lunar_year']);
897
898
            $lessLunarAdjustFactor =
899
                (!$lessLunar['is_leap'] && $lessLunarLeapMonth == $lessLunar['lunar_month']) || $lessLunarLeapMonth > $lessLunar['lunar_month'] ? 1 : 0;
900
            $diff += 12 + $lessLunarAdjustFactor - $lessLunar['lunar_month'];
901
            for ($i = $lessLunar['lunar_year'] + 1; $i < $greaterLunar['lunar_year']; ++$i) {
902
                $diff += $this->monthsOfYear($i);
903
            }
904
            $greaterLunarAdjustFactor =
905
                $greaterLunar['is_leap'] || (0 < $greaterLunarLeapMonth && $greaterLunarLeapMonth < $greaterLunar['lunar_month']) ? 1 : 0;
906
            $diff += $greaterLunarAdjustFactor + $greaterLunar['lunar_month'];
907
        }
908
909
        $diff -= $greaterLunar['lunar_day'] >= $lessLunar['lunar_day'] ? 0 : 1;
910
911
        return $absolute ? $diff : ($changed ? -1 * $diff : $diff);
912
    }
913
914
    /**
915
     * 获取两个日期之间以日为单位的距离.
916
     *
917
     * @param array $lunar1
918
     * @param array $lunar2
919
     * @param bool  $absolute
920
     *
921
     * @return int
922
     */
923
    public function diffInDays($lunar1, $lunar2, $absolute = true)
924
    {
925
        $solar1 =
926
            $this->lunar2solar($lunar1['lunar_year'], $lunar1['lunar_month'], $lunar1['lunar_day'], $lunar1['is_leap']);
927
        $date1 = $this->makeDate("{$solar1['solar_year']}-{$solar1['solar_month']}-{$solar1['solar_day']}");
928
929
        $solar2 =
930
            $this->lunar2solar($lunar2['lunar_year'], $lunar2['lunar_month'], $lunar2['lunar_day'], $lunar2['is_leap']);
931
        $date2 = $this->makeDate("{$solar2['solar_year']}-{$solar2['solar_month']}-{$solar2['solar_day']}");
932
933
        return $date1->diff($date2, $absolute)->format('%r%a');
934
    }
935
936
    /**
937
     * 增加年数.
938
     *
939
     * @param array $lunar
940
     * @param int   $value
941
     * @param bool  $overFlow
942
     *
943
     * @return array
944
     */
945
    public function addYears($lunar, $value = 1, $overFlow = true)
946
    {
947
        $newYear = $lunar['lunar_year'] + $value;
948
        $newMonth = $lunar['lunar_month'];
949
        $newDay = $lunar['lunar_day'];
950
        $isLeap = $lunar['is_leap'];
951
        $needOverFlow = false;
952
953
        $leapMonth = $this->leapMonth($newYear);
954
        $isLeap = $isLeap && $newMonth == $leapMonth;
955
        $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
956
957
        if ($newDay > $maxDays) {
958
            if ($overFlow) {
959
                $newDay = 1;
960
                $needOverFlow = true;
961
            } else {
962
                $newDay = $maxDays;
963
            }
964
        }
965
        $ret = $this->lunar($newYear, $newMonth, $newDay, $isLeap);
966
        if ($needOverFlow) {
967
            $ret = $this->addMonths($ret, 1, $overFlow);
968
        }
969
970
        return $ret;
971
    }
972
973
    /**
974
     * 减少年数.
975
     *
976
     * @param array $lunar
977
     * @param int   $value
978
     * @param bool  $overFlow
979
     *
980
     * @return array
981
     */
982
    public function subYears($lunar, $value = 1, $overFlow = true)
983
    {
984
        return $this->addYears($lunar, -1 * $value, $overFlow);
985
    }
986
987
    /**
988
     * 增加月数.
989
     *
990
     * @param array $lunar
991
     * @param int   $value
992
     * @param bool  $overFlow
993
     *
994
     * @return array
995
     */
996
    public function addMonths($lunar, $value = 1, $overFlow = true)
997
    {
998
        if (0 > $value) {
999
            return $this->subMonths($lunar, -1 * $value, $overFlow);
1000
        } else {
1001
            $newYear = $lunar['lunar_year'];
1002
            $newMonth = $lunar['lunar_month'];
1003
            $newDay = $lunar['lunar_day'];
1004
            $isLeap = $lunar['is_leap'];
1005
1006
            while (0 < $value) {
1007
                $leapMonth = $this->leapMonth($newYear);
1008
                if (0 < $leapMonth) {
1009
                    $currentIsLeap = $isLeap;
1010
                    $isLeap = $newMonth + $value == $leapMonth + ($isLeap ? 0 : 1);
1011
1012
                    if ((!$currentIsLeap && $leapMonth == $newMonth) || ($newMonth < $leapMonth && $newMonth + $value > $leapMonth)) {
1013
                        --$value;
1014
                    }
1015
                } else {
1016
                    $isLeap = false;
1017
                }
1018
1019
                if (13 > $newMonth + $value) {
1020
                    $newMonth += $value;
1021
                    $value = 0;
1022
                } else {
1023
                    $value = $value + $newMonth - 13;
1024
                    ++$newYear;
1025
                    $newMonth = 1;
1026
                }
1027
1028
                if (0 == $value) {
1029
                    $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
1030
                    if ($newDay > $maxDays) {
1031
                        if ($overFlow) {
1032
                            $newDay = 1;
1033
                            ++$value;
1034
                        } else {
1035
                            $newDay = $maxDays;
1036
                        }
1037
                    }
1038
                }
1039
            }
1040
1041
            return $this->lunar($newYear, $newMonth, $newDay, $isLeap);
1042
        }
1043
    }
1044
1045
    /**
1046
     * 减少月数.
1047
     *
1048
     * @param array $lunar
1049
     * @param int   $value
1050
     * @param bool  $overFlow
1051
     *
1052
     * @return array
1053
     */
1054
    public function subMonths($lunar, $value = 1, $overFlow = true)
1055
    {
1056
        if (0 > $value) {
1057
            return $this->addMonths($lunar, -1 * $value, $overFlow);
1058
        } else {
1059
            $newYear = $lunar['lunar_year'];
1060
            $newMonth = $lunar['lunar_month'];
1061
            $newDay = $lunar['lunar_day'];
1062
            $isLeap = $lunar['is_leap'];
1063
            $needOverFlow = false;
1064
1065
            while (0 < $value) {
1066
                $leapMonth = $this->leapMonth($newYear);
1067
1068
                if (0 < $leapMonth) {
1069
                    $isLeap = $newMonth - $value == $leapMonth;
1070
1071
                    if ($newMonth >= $leapMonth && $newMonth - $value < $leapMonth) {
1072
                        --$value;
1073
                    }
1074
                } else {
1075
                    $isLeap = false;
1076
                }
1077
1078
                if ($newMonth > $value) {
1079
                    $newMonth -= $value;
1080
                    $value = 0;
1081
                } else {
1082
                    $value = $value - $newMonth;
1083
                    --$newYear;
1084
                    $newMonth = 12;
1085
                }
1086
1087
                if (0 == $value) {
1088
                    $maxDays = $isLeap ? $this->leapDays($newYear) : $this->lunarDays($newYear, $newMonth);
1089
                    if ($newDay > $maxDays) {
1090
                        $newDay = $maxDays;
1091
                        $needOverFlow = $overFlow;
1092
                    }
1093
                }
1094
            }
1095
1096
            $ret = $this->lunar($newYear, $newMonth, $newDay, $isLeap);
1097
            if ($needOverFlow) {
1098
                $ret = $this->addDays($ret, 1);
1099
            }
1100
1101
            return $ret;
1102
        }
1103
    }
1104
1105
    /**
1106
     * 增加天数.
1107
     *
1108
     * @param array $lunar
1109
     * @param int   $value
1110
     *
1111
     * @return array
1112
     */
1113
    public function addDays($lunar, $value = 1)
1114
    {
1115
        $solar =
1116
            $this->lunar2solar($lunar['lunar_year'], $lunar['lunar_month'], $lunar['lunar_day'], $lunar['is_leap']);
1117
        $date = $this->makeDate("{$solar['solar_year']}-{$solar['solar_month']}-{$solar['solar_day']}");
1118
        $date->modify($value.' day');
1119
1120
        return $this->solar2lunar((int)$date->format('Y'), (int)$date->format('m'), (int)$date->format('d'));
1121
    }
1122
1123
    /**
1124
     * 减少天数.
1125
     *
1126
     * @param array $lunar
1127
     * @param int   $value
1128
     *
1129
     * @return array
1130
     */
1131
    public function subDays($lunar, $value = 1)
1132
    {
1133
        return $this->addDays($lunar, -1 * $value);
1134
    }
1135
1136
    /**
1137
     * 创建日期对象
1138
     *
1139
     * @param string $string
1140
     * @param string $timezone
1141
     *
1142
     * @return \DateTime
1143
     */
1144
    protected function makeDate($string = 'now', $timezone = 'PRC')
1145
    {
1146
        return new DateTime($string, new DateTimeZone($timezone));
1147
    }
1148
1149
    /**
1150
     * 获取时柱.
1151
     *
1152
     * @param int $hour      0~23 小时格式
1153
     * @param int $ganZhiDay 干支日期
1154
     *
1155
     * @return array
1156
     *
1157
     * @see https://baike.baidu.com/item/%E6%97%B6%E6%9F%B1/6274024
1158
     */
1159
    protected function ganZhiHour($hour, $ganZhiDay)
1160
    {
1161
        if (!is_numeric($hour) || $hour < 0 || $hour > 23) {
0 ignored issues
show
The condition is_numeric($hour) is always true.
Loading history...
1162
            return [null, null, null];
1163
        }
1164
1165
        $zhiHour = intval(($hour + 1) / 2);
1166
        $zhiHour = 12 === $zhiHour ? 0 : $zhiHour;
1167
1168
        return [
1169
            $this->gan[($ganZhiDay % 10 % 5 * 2 + $zhiHour) % 10].$this->zhi[$zhiHour],
1170
            $this->zhi[$zhiHour].'时',
1171
            sprintf('%02d', $hour),
1172
        ];
1173
    }
1174
}
1175