dinghunsaker / calends
PHP中的任意日历系统。
Requires
- php: ^5.4|^7.0
- ext-date: *
- ext-gmp: *
- ext-intl: *
- danhunsaker/bcmath: ^1.1.2
- fisharebest/ext-calendar: ^2.2
- rmiller/caser: ^0.1.0
Requires (Dev)
- behat/behat: ^3.0.7
- bossa/phpspec2-expect: ^1.0
- coduo/phpspec-data-provider-extension: ^1.0
- fightbulc/moment: ^1.17
- henrikbjorn/phpspec-code-coverage: ^2.0|^1.0
- illuminate/database: ^5.0.7
- jenssegers/date: ^3.1
- league/period: ^2.5.1|^3.1
- nesbot/carbon: ^1.21
- phpspec/phpspec: ^2.4
- phpunit/phpunit: ^4.0|^5.0
- rmiller/behat-spec: ^0.3
Suggests
- fightbulc/moment: Required to convert to/from Moment\Moment objects (^1.17)
- illuminate/database: Pulls calendar definitions from a database using Laravel's Eloquent ORM. (^5.0.7)
- jenssegers/date: Required to convert to/from Jenssegers\Date\Date objects (^3.1)
- league/period: Required to convert to/from League\Period\Period objects (^2.5.1|^3.1)
- nesbot/carbon: Required to convert to/from Carbon\Carbon objects (^1.21)
README
PHP中的任意日历系统。
安装
使用Composer
composer require danhunsaker/calends
设置
Laravel
包含了一个Laravel服务提供者。将其添加到应用程序的服务提供者列表中,发布Eloquent日历迁移,运行迁移,然后尽情享受吧!
在 config/app.php
'providers' => [ // ... Danhunsaker\Calends\Laravel\ServiceProvider::class, // ... ],
然后在命令行
php artisan vendor:publish --tag=migrations --provider=Danhunsaker\\Calends\\Laravel\\ServiceProvider php artisan migrate
注意,这完全是可选的 - 您可以轻松跳过服务提供者和迁移,并使用Calends而不需要Eloquent日历支持。这会限制您只能使用定义为PHP类的日历,但如果您不需要动态和/或用户定义的日历支持,这不应该成为问题。
您还可以像对Facade一样注册一个类别名
'aliases' => [ // ... 'Calends' => Danhunsaker\Calends\Calends::class, // ... ],
当然,这实际上不是一个Facade。它只是设置了一个别名(使用PHP的class_alias()
函数,但为了轻量级,只在别名首次访问时),这样您就可以在项目中的其他地方访问类时跳过命名空间。这里唯一的真正优势是避免在您的整个应用程序中编写和维护use
语句。
其他项目
一旦您安装了Calends,使用它通常就像创建一个新的Calends
对象并从那里操作它一样简单。如果您想使用自定义日历系统或类转换器,请确保首先注册它们(详情见下文),否则,所有内容都应该直接工作。
您可能需要进行更多设置的主要情况是与Eloquent日历一起。您需要确保在您的项目中安装了illuminate/database
,并且它配置正确以连接到正确的数据库,并且数据库已正确初始化,包含Calends需要存储日历定义的表。
检查extra/calends-init-db.php
以获取设置DB访问的适当环境变量,并确保它们已正确设置。这取决于您的项目如何运行,因此请咨询您的Web服务器或Shell文档以获取有关如何操作的更多详细信息。您还需要在命令行Shell中设置这些,因此下面我们将展示一种方法(但仍然检查extra/calends-init-db.php
,因为我们不会设置所有这些)。一旦您的环境设置完毕,请将文件包含或require在您的项目初始化代码中的某个位置
require_once('vendor/danhunsaker/calends/extra/calends-init-db.php');
最后,在命令行
export DB_DRIVER='mysql' export DB_HOST='localhost' export DB_USER='username' export DB_PASS='password' export DB_NAME='database' php vendor/danhunsaker/calends/extra/non-laravel-migrate.php
这就完成了 - 您的数据库已设置,您的项目已配置为正确连接到它,以便Eloquent日历可以在您的努力下正常工作。下面是如何在实际项目中创建Eloquent日历以供使用的说明。
待办事项
Calends 功能性强,默认支持多种日历系统,但尚未完全准备好投入生产。以下是已知问题的列表以及未来版本的计划。
- 对现有日历的更稳健支持
- 希伯来日历和儒略日历
- 两者都依赖于 PHP 内置的(公历)DateTime 类来解析日期,然后将结果值转换为内部使用的时戳。它们各自应该有自己的解析器(或者可能访问一个基于 Eloquent 解析器的共享解析器),能够处理每个日历的细微差别。两者在日期输出方面也以类似的方式进行,这也应该改为更具体的日历相关的格式。
- 尊重
$format
参数- 目前,只有公历、Eloquent、TAI64、Unix 和儒略日历支持向提供它的方法传递
$format
参数。其他日历也应正确支持这个(尽管是可选的)参数。
- 目前,只有公历、Eloquent、TAI64、Unix 和儒略日历支持向提供它的方法传递
- 希伯来日历和儒略日历
- Eloquent 日历插值支持(有关更多详细信息,请见下文)
- 更多日历系统(可能通过外部库实现)
- 中文(多个变体)
- Discordian
- 中美洲(通常称为玛雅)
- 波斯
- 星历(是的,就是那些来自星际迷航™的;存在多个变体)
使用方法
日期
通过构造函数的参数传递日期和日历系统来创建一个 Calends
对象
use Danhunsaker\Calends\Calends; // The default date is the value of microtime(true), and the default // calendar is 'unix' - the following are equivalent: $now = new Calends(); $now = new Calends(null, 'unix'); $now = new Calends(microtime(true)); $now = new Calends(microtime(true), 'unix'); // You can also use Calends::create() instead of new Calends(), which // can be pretty useful when using method chaining: $now = Calends::create(); $now = Calends::create(null, 'unix'); $now = Calends::create(microtime(true)); $now = Calends::create(microtime(true), 'unix'); // UNIX Epoch - the following are equivalent: $epoch = Calends::create(0); $epoch = Calends::create(0, 'unix'); $epoch = Calends::create(2440587.5, 'jdc'); $epoch = Calends::create('1970-01-01 00:00:00 UTC', 'gregorian'); $epoch = Calends::create('1970-01-01', 'gregorian', 'Y-m-d');
转换
现在您可以将其日期转换为任何其他支持的系统
use Danhunsaker\Calends\Calends; $now = Calends::create(); // Using getDate(): $unix = $now->getDate('unix'); // 1451165670.329400000000000000 // Or just use as a function - __invoke() calls getDate() (but see below...) $unix = $now('unix'); // 1451165670.329400000000000000 // The default 'calendar' for getDate() is also 'unix' $unix = $now(); // 1451165670.329400000000000000 $julianDayCount = $now('jdc'); // 2457383.398962145833333333 $gregorian = $now('gregorian'); // Sat, 26 Dec 2015 14:34:30.3294 -07:00 $gregorian = $now('gregorian', 'Y-m-d_H-i-s-u'); // 2015-12-26_14-34-30-3294 $julianCalendar = $now('julian'); // 12/13/2015 14:34:30 GMT-07:00
请注意,虽然您可以向
Calends::create()
或Calends::getDate()
(及其变体)传递格式,但并非所有日历都会关注它们,每个日历支持的格式代码完全由该日历本身定义。有关更多详细信息,请参阅格式部分。
您还可以将 Calends
对象转换为其他类型的日期/时间对象。这里也有解决方案
use Danhunsaker\Calends\Calends; $now = Calends::create(); $dt = $now->convert('DateTime');
而且,不要认为我们忘记让您以相反的方式转换,从其他日期/时间对象到 Calends
对象,请不要担心
use Danhunsaker\Calends\Calends; $now = Calends::import(new DateTime());
支持转换包括 DateTime
(以及一些衍生物,如 Carbon\Carbon
、Jenssegers\Date\Date
和 Moment\Moment
)、IntlCalendar
(这意味着当然也包括 IntlGregorianCalendar
)、League\Period\Period
。由于 IntlCalendar
至少需要 PHP 5.5,因此在 5.4 中您将无法将其或 IntlGregorianCalendar
转换为/从。但请参阅以下部分了解如何添加对其他日期/时间类的支持。
存储
从技术上讲,您可以在支持的输出格式中的任何一种中存储 Calends 日期值,但这不是推荐的做法,性能就是其中之一。相反,使用内置的 tai
'calendar'(或者,通过将对象转换为 string
来保存)来保存和恢复 Calends
对象
use Danhunsaker\Calends\Calends; $now = Calends::create(); $tai = $now->getDate('tai'); // 40000000567f07e613a23ec000000000 $tai = $now('tai'); // 40000000567f07e613a23ec000000000 $tai = (string) $now; // 40000000567f07e613a23ec000000000 // Save the value of $tai in your database, or wherever makes sense for your app
然后,每次您需要重新创建保存的 Calends
对象时
use Danhunsaker\Calends\Calends; // Retrieve the previously-stored value of $tai... $date = Calends::create($tai, 'tai');
内部使用外部 TAI64NA 格式(或者更确切地说,使用其反序列化版本)来表示所有日期/时间值,因此使用它比在支持的任何其他日历之间进行转换要快得多。但是,请注意,只使用 TAI64NA 格式 - 所表示的秒数仍然是 UTC 秒,而不是 TAI 秒,因为 PHP 目前缺少计算两者之间相关偏移的可靠机制。
为了方便,Calends
实现了 Serializable
和 JsonSerializable
接口,这意味着您也可以安全地对 Calends
对象进行 serialize()
、unserialize()
和 json_encode()
操作——它将自动将其转换为(并在 unserialize()
的情况下从)tai
日期。不过,关于这一点,下面还有更多内容...
格式化
到现在为止,你可能已经注意到,解析和输出日期字符串的方法都接受日历和格式。如上所述,一些日历系统会静默忽略此值,替换它们自己的硬编码值。截至本文写作时,处于这种位置的唯一日历是希伯来历和儒略历。然而,每个日历系统的有效格式字符串仍然非常特定于日历。所以下面是每个内置日历系统的有效格式的列表。
- 公历
- PHP 的
date()
函数支持的任何内容。如果传递了格式,Calends 使用date_create_from_format()
解析日期字符串,并且始终使用date()
生成输出字符串。默认输出格式为D, d M Y H:i:s.u P
。
- PHP 的
- 儒略日计数
- 不要与儒略历混淆,JDC 系统支持大量的输入和输出格式。因为这些格式都涉及从默认结果偏移,因此在输入时指定正确的格式非常重要,因为内部计算出的值可能会有很大差异。以下所有偏移量均从维基百科作为快速参考点——如果您发现任何不准确之处,请尽快告知我。
JD
/GJD
/Geo
/Geo-Centric
(默认值,如果没有提供或不可识别时使用)- '规范'的儒略日计数,从公元前 4713 年 1 月 1 日中午(儒略历)起计算天数
RJD
/Reduced
- 没有前两位数字的地球中心 JDC,在大多数常见情况下为 24
MJD
/Modified
- 与简化的 JDC 相同,但开始于午夜而不是中午
TJD
/Truncated
- 与地球中心 JDC 相同,但没有前三位数字或任何分数天
DJD
/Dublin
/J1900
- 从公元 1899 年 12 月 31 日中午(公历)开始计算
J2000
- 从公元 2000 年 1 月 1 日中午(公历)开始
Lilian
- 从公历设立以来计算的天数,公元 1582 年 10 月 15 日午夜(公历);没有分数天
Rata-Die
- 自公元 0001 年 1 月 1 日午夜(拟制公历)以来的天数;没有分数天
Mars-Sol
- 火星日(即火星上的天数,长度与地球上的不同;也称为'sols'而不是'days')自公元 1873 年 12 月 29 日中午(公历)起
- TAI64
- 对于
tai
日期,有四种格式可用。其中三种是在 TAI64 规范中定义的十六进制格式,它们在编码的值精度上有所不同。第四种是具有完整 18 位精度的十进制(基数 10)输出格式。请注意,再次强调,TAI 格式不提供 TAI 秒的值——这些是 TAI64 范围和格式中的 UTC 秒。如果/当有可靠的转换机制可用时,这将发生变化,并且tai
日历系统将提供实际的 TAI 秒。 TAI64
- 二级精度;没有分数秒被编码。此格式正好 16 个字符长。
TAI64N
- 纳秒级精度;分数秒被编码到十亿分之一。此格式正好 24 个字符长。
TAI64NA
(默认值,如果没有提供或不可识别时使用)- 阿秒级精度;分数秒被编码到可用的全部十万亿分之一。此格式正好 32 个字符长。
数值
- 输出时间戳的十进制(基数 10)值,具有完整精度。
- 对于
- Unix
- 这个非常简单直接。格式实际上是一个精度指定符,告诉Calends返回小数点后多少位。有效值介于
0
和18
之间;所有其他值都将被强制进入这个范围,如果没有提供值,则使用18
。
- 这个非常简单直接。格式实际上是一个精度指定符,告诉Calends返回小数点后多少位。有效值介于
- 优雅的
- 以下是如何定义这些内容,并请查阅您的特定日历数据以了解实际允许的值及其含义。
比较
通常,比较两个日期以查看哪个先来是非常有用的。一个很好的例子就是排序。Calends就是为此而设计的,支持四种不同的日期比较方法。由于排序非常常见,所以我们从为此设计的那个方法开始。
use Danhunsaker\Calends\Calends; $times = []; for ($i = 0; $i < 10; $i++) { $times[] = Calends::create(mt_rand(0 - mt_getrandmax(), mt_getrandmax())); } print_r($times); $sorted = usort($times, [Calends::class, 'compare']); print_r($sorted);
Calends::compare()
接受两个要比较的Calends
对象,如果第一个对象在第二个之前返回-1,如果它们相等返回0,如果第一个对象在第二个之后返回+1。这与PHP的排序函数及其对排序回调行为的要求兼容。
以下三个方法提供更集中的比较,返回true
或false
而不是较小/等于/较大。
use Danhunsaker\Calends\Calends; $epoch = Calends::create(0); $now = Calends::create(); print_r([ $epoch::isBefore($now), // true $epoch::isSame($now), // false $epoch::isAfter($now), // false ]);
这些方法中的每一个都接受要比较的Calends
对象,并返回一个布尔值,如上所述。
还有一个你可以用来比较Calends
对象的方法,它返回两个对象差异的秒数,而不仅仅是它们差异的方向。
use Danhunsaker\Calends\Calends; $epoch = Calends::create(0); $now = Calends::create(); echo $now->difference($epoch); // Seconds between $epoch and $now
修改
Calends
对象是不可变的——也就是说,你不能直接修改它们。相反,任何会改变对象值的行为实际上都会创建并返回一个全新的Calends
对象。这有一些优点,比如保留原始对象,但如果你不知道这一点,可能会有些意外。下面的示例考虑了这一点。
use Danhunsaker\Calends\Calends; $now = Calends::create(); $tomorrow = $now->add('1 day', 'gregorian'); $yesterday = $now->subtract('1 day', 'gregorian'); $last24hrs = $now->setDate($yesterday->getDate());
范围
最后一个示例实际上引入了Calends
对象的一个特性,我们之前没有提到。每个日期值都可以看作是时间的一个瞬间——一个既开始又结束在同一时间值的时间范围。这样一个范围的时间长度为零。Calends利用这个事实,可以像处理单个时间点一样轻松地处理每个Calends
对象作为范围。这意味着时间和范围可以共存于一个类中,从而使复杂操作大大简化。
这也意味着,除了有其他一些你可以用来执行范围相关操作的方法之外,你之前已经学到的许多方法在你想用范围而不是简单日期进行工作时也有额外的使用方法。
让我们从最后一个示例开始,提供一些更多关于它在做什么的细节,并使用它来介绍一些其他方法。
use Danhunsaker\Calends\Calends; $now = Calends::create(); $tomorrow = $now->add('1 day', 'gregorian'); // tomorrow to today // (duration: -1 day) $yesterday = $now->subtract('1 day', 'gregorian'); // yesterday to today // (duration: 1 day) $endsTomorrow = $now->addFromEnd('1 day', 'gregorian'); // today to tomorrow // (duration: 1 day) $endsYesterday = $now->subtractFromEnd('1 day', 'gregorian'); // today to yesterday // (duration: -1 day)
setDate()
和setEndDate()
也接受一个日历,默认为unix
。未设置的任一端点将从调用实例复制。
use Danhunsaker\Calends\Calends; $now = Calends::create(); $tomorrow = $now->add('1 day', 'gregorian'); $yesterday = $now->subtract('1 day', 'gregorian'); $last24hrs = $now->setDate($yesterday->getDate('gregorian'), 'gregorian'); // yesterday to today; same as $yesterday $next24hrs = $now->setEndDate($tomorrow->getDate('gregorian'), 'gregorian'); // today to tomorrow $next72hrs = $now->setDuration('72 hours', 'gregorian'); // today to three days from now $last72hrs = $now->setDurationFromEnd('72 hours', 'gregorian'); // three days ago to today
你还可以一步创建一个完整的范围。
use Danhunsaker\Calends\Calends; $next7days = Calends::create(['start' => 'now', 'end' => 'now +7 days'], 'gregorian'); $last7days = Calends::create(['start' => 'now -7 days', 'end' => 'now'], 'gregorian');
当然,你可能还想能够检索结束日期和持续时间,以及起始日期。
use Danhunsaker\Calends\Calends; $now = Calends::create(); $next72hrs = $now->setDuration('72 hours', 'gregorian'); $endArray = $next72hrs->getInternalEndTime(); // Like getInternalTime() $dateIn72 = $next72hrs->getEndDate('gregorian'); // Like getDate() $secsIn72 = $next72hrs->getDuration(); // In seconds
此外,当持续时间不为0时,将Calends
对象作为函数调用,将返回对象的起始点和结束点(我们提到我们会在“下面”有更多关于这种用法的介绍——这就是它!)
use Danhunsaker\Calends\Calends; $now = Calends::create(); $next72hrs = $now->setDuration('72 hours', 'gregorian'); $endpoints = $next72hrs('gregorian'); // ['start' => ..., 'end' => ...]
虽然从setDate()
生成的新的Calends
对象继承了创建它的对象的结束日期,而从setEndDate()
生成的新的对象继承了创建者的起始日期(这意味着这些新对象重叠),但有时你想要创建与创建对象相邻的新对象。以下是方法。
use Danhunsaker\Calends\Calends; $next7days = Calends::create(['start' => 'now', 'end' => 'now +7 days'], 'gregorian'); $last7days = Calends::create(['start' => 'now -7 days', 'end' => 'now'], 'gregorian'); $followingWeek = $next7days->next(); $precedingWeek = $last7days->previous(); $followingMonth = $next7days->next('1 month', 'gregorian'); $precedingMonth = $last7days->previous('1 month', 'gregorian');
如果您想使用组合范围,我们已为您做好准备
use Danhunsaker\Calends\Calends; $next7days = Calends::create(['start' => 'now', 'end' => 'now +7 days'], 'gregorian'); $last7days = Calends::create(['start' => 'now -7 days', 'end' => 'now'], 'gregorian'); $precedingWeek = $last7days->previous(); $followingMonth = $next7days->next('1 month', 'gregorian'); $precedingMonth = $last7days->previous('1 month', 'gregorian'); $bothMonths = $precedingMonth->merge($followingMonth); // 2.5 months $commonTime = $precedingMonth->intersect($precedingWeek); // 1 week $betweenMonths = $precedingMonth->gap($followingMonth); // 2 weeks
但请注意,如果您在没有重叠的情况下调用 intersect()
或在存在重叠时调用 gap()
,则会抛出 InvalidCompositeRangeException
use Danhunsaker\Calends\Calends; $next7days = Calends::create(['start' => 'now', 'end' => 'now +7 days'], 'gregorian'); $last7days = Calends::create(['start' => 'now -7 days', 'end' => 'now'], 'gregorian'); $precedingWeek = $last7days->previous(); $followingMonth = $next7days->next('1 month', 'gregorian'); $precedingMonth = $last7days->previous('1 month', 'gregorian'); $invalidRange = $precedingMonth->intersect($followingMonth); // Exception $invalidRange = $precedingMonth->gap($precedingWeek); // Exception
那么,一个日期范围库如果没有范围比较功能,岂不是不完整?
use Danhunsaker\Calends\Calends; $now = Calends::create(); $last24hrs = $now->subtract('1 day', 'gregorian'); print_r([ $now->startsBefore($last24hrs), // false $now->isBefore($last24hrs), // false $now->endsBefore($last24hrs), // false $now->isSame($last24hrs), // false $now->startsDuring($last24hrs), // true $now->isDuring($last24hrs), // true $now->endsDuring($last24hrs), // true $now->contains($last24hrs), // false $now->overlaps($last24hrs), // true $now->abuts($last24hrs), // false $now->startsAfter($last24hrs), // true $now->isAfter($last24hrs), // false $now->endsAfter($last24hrs), // false $now->isLonger($last24hrs), // false $now->isShorter($last24hrs), // true $now->isSameDuration($last24hrs), // false ]);
为了实现所有这些功能,我们需要一个更灵活的 compare()
方法
use Danhunsaker\Calends\Calends; $times = []; for ($i = 0; $i < 10; $i++) { $times[] = Calends::create([ 'start' => mt_rand(0 - mt_getrandmax(), mt_getrandmax()), 'end' => mt_rand(0 - mt_getrandmax(), mt_getrandmax()) ]); } print_r($times); $sorted = usort($times, function($a, $b) { return Calends::compare($a, $b, 'start'); }); print_r($sorted); // Sorted by start date, which is the default $endSorted = usort($times, function($a, $b) { return Calends::compare($a, $b, 'end'); }); print_r($endSorted); // Sorted by end date $endStartSorted = usort($times, function($a, $b) { return Calends::compare($a, $b, 'end-start'); }); print_r($endStartSorted); // Ranges that start before others end are earlier // in this sort $startEndSorted = usort($times, function($a, $b) { return Calends::compare($a, $b, 'start-end'); }); print_r($startEndSorted); // Ranges that end before others start are earlier // in this sort $durationSorted = usort($times, function($a, $b) { return Calends::compare($a, $b, 'duration'); }); print_r($durationSorted); // Sorted by duration
当然,这也意味着我们希望我们的 difference()
方法具有相同的灵活性
use Danhunsaker\Calends\Calends; $now = Calends::create(); $next7days = Calends::create(['start' => 'now', 'end' => 'now +7 days'], 'gregorian'); $last7days = Calends::create(['start' => 'now -7 days', 'end' => 'now'], 'gregorian'); echo $now->difference($next7days, 'start'); // 0 echo $now->difference($next7days, 'end'); // 604800 echo $last7days->difference($next7days, 'start-end'); // 1209600 echo $last7days->difference($next7days, 'end-start'); // 0 echo $last7days->difference($next7days, 'duration'); // 0
现在,在存储部分,我们提到在下面将介绍更多关于序列化 Calends
对象的内容。好吧,这就是“下面”。因为 Calends
对象实际上是范围,序列化的值可能不总是简单的 TAI64NA 字符串。如果结束日期与开始日期不匹配,则将序列化一个数组,包含 'start'
和 'end'
键。这使得即使 Calends
范围也可以轻松恢复。一般来说,您不必过多担心这种行为。
新的日历
类定义
提供新的日历定义有两种方式。第一种,也是最灵活的方式,是通过实现 Danhunsaker\Calends\Calendar\DefinitionInterface
的类。实际上,Calends 中的日历就是这样构建的。一旦您的日历定义类在您的项目中可用,您需要使用 Calends::registerCalendar()
来注册它
use Danhunsaker\Calends\Calends; Calends::registerCalendar('myCustomCalendar', MyCustomCalendar::class);
这将使您的日历系统对项目中的所有 Calends
对象都可用。
请注意,虽然 Calends 会自动找到并注册
Danhunsaker\Calends\Calendar
命名空间中的类定义,但如果这些类不是由主要项目官方认可,则不建议在那里创建类,因为命名空间意味着官方支持或认可。
数据库定义
另一种方式是将您的定义存储在数据库中。要使用这种方法,您需要在项目中包含 illuminate/database
。(这个库是 Laravel 框架的一部分,所以您可能已经有了它。)使用这种方法需要更多的工作,但在您希望允许用户在项目中定义自己的日历系统,而不需要他们编写任何代码的情况下,它可以非常有用。
对 Laravel 用户的一个快速提醒,在使用数据库定义之前,请按照本 README 文件顶部的设置说明操作,安装适当的迁移等!
在数据库中定义日历有两种基本方法。第一种是直接在数据库中设置适当的值,无论是通过 DB seed 文件、使用 MySQL 客户端或 phpMySQL 等工具手动输入,还是使用一个或多个 .sql 导出文件,或其他方法。这种方法很灵活,但对不是开发者的用户来说并不非常用户友好。接下来是第二种方法——使用构成数据库定义功能核心的 Eloquent 模型的程序创建。本 README 文件将重点介绍后者;前者方法可以从 Eloquent 的工作方式中很容易地得出结论,并且您始终可以查看 [TestHelpers 类文件][tests/TestHelpers.php] 以获取不使用 Eloquent 定义日历的更复杂示例。
日历
一切从 Danhunsaker\Calends\Eloquent\Calendar
开始。这个类既实现了 ObjectDefinitionInterface
接口(见下文),也是其他模型通过它访问的核心 Eloquent 模型。首先使用 Calendar
类来创建一个新的(空的)日历定义。
use Danhunsaker\Calends\Eloquent\Calendar; $calendar = Calendar::create([ 'name' => 'example', 'description' => 'A simple example calendar.', ]);
name
的含义基本上就是所说的那样 - 您将使用它来告诉 Calends
使用哪个日历定义。description
是可选的,所以如果您愿意可以跳过。
单元
现在我们已经创建了一个 Calendar
,我们可以开始向其中添加 Unit
。
$second = $calendar->units()->create([ 'internal_name' => 'second', 'scale_amount' => 1, 'scale_inverse' => false, 'scale_to' => 0, 'uses_zero' => true, 'unix_epoch' => 0, 'is_auxiliary' => false, ]);
-
internal_name
应该在日历中是唯一的,因为它将用于跟踪哪些值属于哪些单元。强烈建议使用可读性强的名称,因为它用于偏移量解析(下文将详细介绍)。 -
scale_amount
、scale_inverse
和scale_to
一起定义了如何相对于其他Unit
来计算这个Unit
。scale_to
指定要计算的Unit
,通常是适当Unit
的id
。然而,对于指定Unit
与 UNIX 时间戳秒的关系,有一个特殊情况。在这种情况下,您将直接设置scale_to
为值0
,而不是通过 Eloquent 关系添加另一个Unit
。scale_amount
指定一个Unit
中有多少个scale_to
;scale_inverse
指定scale_amount
实际上是单个scale_to
中的Unit
数量。不用担心这还没有什么意义;下文有更多示例可以帮助您更清晰地理解。 -
uses_zero
指定Unit
是否从 0 开始计数。如果设置为 false,则假设Unit
从 1 开始计数。作为一个有用的例子,考虑秒从 0 开始计数,但月份从 1 开始。(注意:虽然使用它来表示年份或小时不使用零很有吸引力,但如果这样使用,数学就开始崩溃了。相反,设置Era
来覆盖这些场景(见下文)。 -
unix_epoch
指定这个Unit
在 UNIX 埃泼克(UTC 时间 1970 年 1 月 1 日 00:00:00)时的值。这使Calends
能够确定给定日期相对于其他日期的位置,并且对于一旦解析后能够将日期转换为其他日历系统等操作至关重要。尽管如此,该值是可选的,因为并非每个Unit
都需要指定其埃泼克值(特别是,辅助Unit
埃泼克值通常应计算得出)。 -
is_auxiliary
告诉Calends
给定的Unit
是一个派生的,而不是日期值的基本组成部分。这对于像周(对于周不是日期重要性的日历系统)、世纪和飞秒这样的场景很有用,在这些场景中,计算值很有用,但对日期计算本身不是必要的。
让我们添加更多内容,以便我们有一些更实质性的东西可以操作。
$minute = $calendar->units()->create([ 'internal_name' => 'minute', 'scale_amount' => 60, 'scale_inverse' => false, 'uses_zero' => true, 'unix_epoch' => 0, 'is_auxiliary' => false, ]); $minute->scaleMeTo()->save($second); $hour = $calendar->units()->create([ 'internal_name' => 'hour', 'scale_amount' => 60, 'scale_inverse' => false, 'uses_zero' => true, 'unix_epoch' => 0, 'is_auxiliary' => false, ]); $hour->scaleMeTo()->save($minute); $day = $calendar->units()->create([ 'internal_name' => 'day', 'scale_amount' => 24, 'scale_inverse' => false, 'uses_zero' => false, 'unix_epoch' => 1, 'is_auxiliary' => false, ]); $day->scaleMeTo()->save($hour); $month = $calendar->units()->create([ 'internal_name' => 'month', 'scale_amount' => null, // See below for why this works 'scale_inverse' => false, 'uses_zero' => false, 'unix_epoch' => 1, 'is_auxiliary' => false, ]); $month->scaleMeTo()->save($day); $year = $calendar->units()->create([ 'internal_name' => 'year', 'scale_amount' => 12, 'scale_inverse' => false, 'uses_zero' => true, 'unix_epoch' => 1970, 'is_auxiliary' => false, ]); $year->scaleMeTo()->save($month); $century = $calendar->units()->create([ 'internal_name' => 'century', 'scale_amount' => 100, 'scale_inverse' => false, 'uses_zero' => true, 'unix_epoch' => null, 'is_auxiliary' => true, ]); $century->scaleMeTo()->save($year); $millisecond = $calendar->units()->create([ 'internal_name' => 'millisecond', 'scale_amount' => 1000, 'scale_inverse' => true, 'uses_zero' => true, 'unix_epoch' => null, 'is_auxiliary' => true, ]); $millisecond->scaleMeTo()->save($second);
那里看起来有很多事情在进行,但实际上大多数都是针对每个 单元
重复相同的事情。这正是数据库的特性。其中也有一些辅助 单元
的例子,何时使用 uses_zero
或不使用,以及甚至倒置的刻度。然而,还有一个特殊案例我们还没有讨论。对于月份 单元
,scale_amount
被设置为 null
。这是因为月份 单元
中天数 单元
的数量每个月都不同。因此,我们需要一种方法让 Calends
知道长度不是固定的。这样它就会知道检查 UnitLength
,并相应地处理。
单元长度
让我们定义我们的月份 单元长度
。
$month->lengths()->createMany([ ['unit_value' => 1, 'scale_amount' => 31], ['unit_value' => 2, 'scale_amount' => 28], ['unit_value' => 3, 'scale_amount' => 31], ['unit_value' => 4, 'scale_amount' => 30], ['unit_value' => 5, 'scale_amount' => 31], ['unit_value' => 6, 'scale_amount' => 30], ['unit_value' => 7, 'scale_amount' => 31], ['unit_value' => 8, 'scale_amount' => 31], ['unit_value' => 9, 'scale_amount' => 30], ['unit_value' => 10, 'scale_amount' => 31], ['unit_value' => 11, 'scale_amount' => 30], ['unit_value' => 12, 'scale_amount' => 31], ]);
这里相当直接。 unit_value
是 scale_amount
指定的正确长度所对应的 单元
的值。
格式化和解析
现在我们已经设置好了所有这些,Calends
可以开始计算新日历系统中的日期。当然,除非我们能够查看并处理这些日期,否则这并不太有用,所以接下来我们需要定义一些格式。
Calends
格式采用分层方法。 CalendarFormat
指定完整日期的 format_string
,使用与 PHP 的 date()
函数类似的语法。 FragmentFormat
实际定义了 CalendarFormat
使用的单个字符格式化代码,而 FragmentText
提供了一种将数值映射到任意文本字符串的方法。但为什么叫它们 FragmentFormat
呢?
时代和时代范围
一些 单元
使用非线性编号显示和书写。例如,格里高利日历系统中的年份对于 1 年以上的年份按正常顺序编号,并分配给公元时代。但对于 1 年以下的年份,它们按从 1 开始的降序编号,而不是从 0 开始,因此公元前的年份显示为 1 BC。我们需要正确处理这些编号方案;进入 时代
和 时代范围
。
时代范围
用于指定一个时代的 start_value
和 end_value
,显示值增加的 direction
,以及将起始 单元
值映射到 start_display
的值。这些属性还与一个内部 range_code
关联,用于识别给定的范围属于哪个时代 - 这在我们将要探讨的许多情况下很有用。时代
简单地将 时代范围
分组,为它们提供一个共同的 internal_name
,并指定在解析给定的日期时不显式包含代码时使用的内部时代代码(即,默认范围)。
让我们创建几个 时代
。
$yearsEra = $year->eras()->create([ 'internal_name' => 'gregorian-years', 'default_range' => 'ad' ]); $hoursEra = $hour->eras()->create([ 'internal_name' => '12-hour-time', 'default_range' => 'am' ]); $yearsEra->ranges()->createMany([ [ 'range_code' => 'bc', 'start_value' => 0, 'end_value' => null, 'start_display' => 1, 'direction' => 'desc' ], [ 'range_code' => 'ad', 'start_value' => 1, 'end_value' => null, 'start_display' => 1, 'direction' => 'asc' ] ]); $hoursEra->ranges()->createMany([ [ 'range_code' => 'am', 'start_value' => 0, 'end_value' => 0, 'start_display' => 12, 'direction' => 'asc' ], [ 'range_code' => 'am', 'start_value' => 1, 'end_value' => 11, 'start_display' => 1, 'direction' => 'asc' ], [ 'range_code' => 'pm', 'start_value' => 12, 'end_value' => 12, 'start_display' => 12, 'direction' => 'asc' ], [ 'range_code' => 'pm', 'start_value' => 13, 'end_value' => 23, 'start_display' => 1, 'direction' => 'asc' ], [ 'range_code' => 'am', 'start_value' => 24, 'end_value' => 24, 'start_display' => 12, 'direction' => 'asc' ] ]);
片段格式和片段文本
现在回到 FragmentFormat
的问题。一个 时代
可以像 单元
一样成为格式的目标,因此在这里支持两者都同等合理的格式化方法是有意义的。每个都是一个完整日期的一部分,所以称之为 FragmentFormat
也很有道理。让我们构建支持 PHP 的 date()
所支持格式代码的子集。
$fragments = [ 'd' => $calendar->fragments()->create([ 'format_code' => 'd', 'format_string' => '%{value}$02d', 'description' => 'Day of the month, 2 digits with leading zeros', ]), 'j' => $calendar->fragments()->create([ 'format_code' => 'j', 'format_string' => '%{value}$d', 'description' => 'Day of the month without leading zeros', ]), 'F' => $calendar->fragments()->create([ 'format_code' => 'F', 'format_string' => '%{value}$s', 'description' => 'A full textual representation of a month, such as January or March', ]), 'm' => $calendar->fragments()->create([ 'format_code' => 'm', 'format_string' => '%{value}$02d', 'description' => 'Numeric representation of a month, with leading zeros', ]), 'M' => $calendar->fragments()->create([ 'format_code' => 'M', 'format_string' => '%{value}$s', 'description' => 'A short textual representation of a month, three letters', ]), 'n' => $calendar->fragments()->create([ 'format_code' => 'n', 'format_string' => '%{value}$d', 'description' => 'Numeric representation of a month, without leading zeros', ]), 't' => $calendar->fragments()->create([ 'format_code' => 't', 'format_string' => '%{length}$d', 'description' => 'Number of days in the given month', ]), 'Y' => $calendar->fragments()->create([ 'format_code' => 'Y', 'format_string' => '%{value}$04d', 'description' => 'A full numeric representation of a year, 4 digits', ]), 'y' => $calendar->fragments()->create([ 'format_code' => 'y', 'format_string' => '%{value}%100$02d', 'description' => 'A two digit representation of a year', ]), 'E' => $calendar->fragments()->create([ 'format_code' => 'E', 'format_string' => '%{code}$s', 'description' => 'The calendar epoch (BC/AD)', ]), 'a' => $calendar->fragments()->create([ 'format_code' => 'a', 'format_string' => '%{code}$s', 'description' => 'Lowercase Ante meridiem and Post meridiem', ]), 'A' => $calendar->fragments()->create([ 'format_code' => 'A', 'format_string' => '%{code}$s', 'description' => 'Uppercase Ante meridiem and Post meridiem', ]), 'g' => $calendar->fragments()->create([ 'format_code' => 'g', 'format_string' => '%{value}$d', 'description' => '12-hour format of an hour without leading zeros', ]), 'G' => $calendar->fragments()->create([ 'format_code' => 'G', 'format_string' => '%{value}$d', 'description' => '24-hour format of an hour without leading zeros', ]), 'h' => $calendar->fragments()->create([ 'format_code' => 'h', 'format_string' => '%{value}$02d', 'description' => '12-hour format of an hour with leading zeros', ]), 'H' => $calendar->fragments()->create([ 'format_code' => 'H', 'format_string' => '%{value}$02d', 'description' => '24-hour format of an hour with leading zeros', ]), 'i' => $calendar->fragments()->create([ 'format_code' => 'i', 'format_string' => '%{value}$02d', 'description' => 'Minutes with leading zeros', ]), 's' => $calendar->fragments()->create([ 'format_code' => 's', 'format_string' => '%{value}$02d', 'description' => 'Seconds, with leading zeros', ]), ]; $fragments['d']->fragment()->save($day); $fragments['j']->fragment()->save($day); $fragments['F']->fragment()->save($month); $fragments['m']->fragment()->save($month); $fragments['M']->fragment()->save($month); $fragments['n']->fragment()->save($month); $fragments['t']->fragment()->save($month); $fragments['Y']->fragment()->save($yearsEra); $fragments['y']->fragment()->save($yearsEra); $fragments['E']->fragment()->save($yearsEra); $fragments['a']->fragment()->save($hoursEra); $fragments['A']->fragment()->save($hoursEra); $fragments['g']->fragment()->save($hoursEra); $fragments['G']->fragment()->save($hour); $fragments['h']->fragment()->save($hoursEra); $fragments['H']->fragment()->save($hour); $fragments['i']->fragment()->save($minute); $fragments['s']->fragment()->save($second); $fragments['F']->texts()->createMany([ ['fragment_value' => 1, 'fragment_text' => 'January'], ['fragment_value' => 2, 'fragment_text' => 'February'], ['fragment_value' => 3, 'fragment_text' => 'March'], ['fragment_value' => 4, 'fragment_text' => 'April'], ['fragment_value' => 5, 'fragment_text' => 'May'], ['fragment_value' => 6, 'fragment_text' => 'June'], ['fragment_value' => 7, 'fragment_text' => 'July'], ['fragment_value' => 8, 'fragment_text' => 'August'], ['fragment_value' => 9, 'fragment_text' => 'September'], ['fragment_value' => 10, 'fragment_text' => 'October'], ['fragment_value' => 11, 'fragment_text' => 'November'], ['fragment_value' => 12, 'fragment_text' => 'December'] ]); $fragments['M']->texts()->createMany([ ['fragment_value' => 1, 'fragment_text' => 'Jan'], ['fragment_value' => 2, 'fragment_text' => 'Feb'], ['fragment_value' => 3, 'fragment_text' => 'Mar'], ['fragment_value' => 4, 'fragment_text' => 'Apr'], ['fragment_value' => 5, 'fragment_text' => 'May'], ['fragment_value' => 6, 'fragment_text' => 'Jun'], ['fragment_value' => 7, 'fragment_text' => 'Jul'], ['fragment_value' => 8, 'fragment_text' => 'Aug'], ['fragment_value' => 9, 'fragment_text' => 'Sep'], ['fragment_value' => 10, 'fragment_text' => 'Oct'], ['fragment_value' => 11, 'fragment_text' => 'Nov'], ['fragment_value' => 12, 'fragment_text' => 'Dec'] ]); $fragments['E']->texts()->createMany([ ['fragment_value' => 'bc', 'fragment_text' => 'BC'], ['fragment_value' => 'ad', 'fragment_text' => 'AD'] ]); $fragments['a']->texts()->createMany([ ['fragment_value' => 'am', 'fragment_text' => 'am'], ['fragment_value' => 'pm', 'fragment_text' => 'pm'] ]); $fragments['A']->texts()->createMany([ ['fragment_value' => 'am', 'fragment_text' => 'AM'], ['fragment_value' => 'pm', 'fragment_text' => 'PM'] ]);
日历格式
当然,还有 CalendarFormat
。
$defaultFormat = $calendar->formats()->create([ 'format_name' => 'eloquent', 'format_string' => 'd M Y H:i:s', 'description' => 'A basic date format' ]); $calendar->formats()->create([ 'format_name' => 'mod8601', 'format_string' => 'Y-m-d H:i:s', 'description' => 'A modified ISO 8601 date' ]); $calendar->formats()->create([ 'format_name' => 'filestr', 'format_string' => 'Y-m-d_H-i-s', 'description' => 'A date suitable for use in filenames' ]); $calendar->defaultFormat->save($defaultFormat);
关于格式的更多内容
再次强调,有很多重复的内容。《FragmentFormat》有一个 format_code
,这是之前提到的单字符日期格式化代码,一个 format_string
,它告诉 Calends
如何渲染值(稍后会详细介绍),以及一个可选的 description
。
format_string
是 PHP 的 sprintf()
函数家族使用的格式的扩展版本。在该规范中,整数值指定了在表达式的给定部分中使用哪个编号的参数,而 Calends
期望一个与 BC::math 的 BC::parse()
方法兼容的公式(BC::math 是 Calends 的依赖项)。它将传递一些正在渲染的片段属性,如 length
和 value
,在 Era
片段的情况下,还有范围 code
。实际上渲染到日期的相应部分的是这个表达式的结果。上面给出了几个例子。
实际上,将每个片段对象分配给每个 FragmentFormat
是很重要的,否则整个系统都会崩溃。这在上面的 $fragments[<code>]->fragment()->save(<fragment object>)
语句中完成。
FragmentText
非常简单——如前所述,一个要转换成相关 fragment_text
的 fragment_value
。
接下来是 CalendarFormat
。一个 format_name
,为 getDate()
提供一个容易记住的别名,一个实际的(与 PHP date()
兼容的)format_string
,它告诉 Calends
正确的渲染格式,以及一个可选的 description
。
现在你可以轻松地在你的新日历系统中解析和格式化日期!渲染格式会根据需要自动反向工程为解析格式,所以无需担心定义这些。
日期偏移量和 UnitName
当然,目前还有一个尚未探索的场景:日期偏移量。由于 Unit
的 internal_name
,最基本的偏移量已经是可解析的。但你的单位的其他复数形式和替代名称怎么办?不用担心,UnitName
就是为了这个目的而设计的。让我们给我们的日历定义添加一些
$second->names()->create([ 'unit_name' => 'seconds', 'name_context' => 'plural' ]); $minute->names()->create([ 'unit_name' => 'minutes', 'name_context' => 'plural' ]); $hour->names()->create([ 'unit_name' => 'hours', 'name_context' => 'plural' ]); $day->names()->create([ 'unit_name' => 'days', 'name_context' => 'plural' ]); $month->names()->create([ 'unit_name' => 'months', 'name_context' => 'plural' ]); $year->names()->create([ 'unit_name' => 'years', 'name_context' => 'plural' ]); $century->names()->create([ 'unit_name' => 'centuries', 'name_context' => 'plural' ]); $millisecond->names()->create([ 'unit_name' => 'milliseconds', 'name_context' => 'plural' ]);
每个 UnitName
提供了偏移量解析的替代 unit_name
,以及一个可选的 name_context
,它目前在 Calends
本身内未使用,但在国际化等情况下可能很有用。
待办事项
细心的读者可能会注意到,我们从未涉及另一种在日历系统中经常遇到的特殊情况,这使得像这样的日期库特别难以编写。这个特殊情况被称为“置入法”,指的是时间单位从基本日历中插入、删除或以其他方式更改。也许最著名的例子是你们很多人都会注意到的上面缺失的一个——闰日。就置入法而言,2月29日(实际上是由于古罗马人在尤利乌斯·恺撒时代设置的方式,但仍然是)相当基础。支持这个特定的置入法并不需要太多,但世界上还有其他更复杂的置入法,从希伯来日历的置入月和月份长度变化,到经常被忽视的闰秒,我们的目标是支持这些类型的置入法。因此,目前置入法完全未实现,并将包含在未来版本中。
- 待办事项:实现置入法,并在此处进行文档记录。
如果您注意到有任何遗漏的地方,请随时在 GitHub 上提交一个 issue 并告诉我。有些功能超出了项目范围,但我很乐意考虑所有选项!
对象定义
当然,您也可以直接在 任何 您想要的类上实现 Eloquent 模型使用的接口(Danhunsaker\Calends\Calendar\ObjectDefinitionInterface
),并将 那个 类的实例注册来处理各种日历。这个接口是为数据库使用而设计的,但这并不意味着它不能在其他地方使用。使用这样的类的示例如下
use Danhunsaker\Calends\Calends; Calends::registerCalendar('myCustomCalendar', new MyCustomCalendar($params));
新的转换器
首先创建一个实现了 Danhunsaker\Calends\Converter\ConverterInterface
的类,就像内置的转换器一样。一旦您的转换器类在项目中可用,只需使用 Calends::registerConverter()
进行注册即可
use Danhunsaker\Calends\Calends; Calends::registerConverter('myDateTimeClass', MyConverter::class);
就像新的日历一样,这将使您的转换器在项目的所有 Calends
对象中可用。
请注意,虽然 Calends 会自动在
Danhunsaker\Calends\Converter
命名空间中找到并注册转换器,但如果它们没有得到主项目的官方认可,除非它们是官方认可的,否则在那些命名空间中创建类被认为是不好的做法,因为命名空间意味着官方支持或认可。
贡献
在 GitHub 上欢迎 pull requests、错误报告等。
安全问题应直接报告给 danhunsaker (plus) calends (at) gmail (dot) com。
其他所有内容,请访问 GitHub。