brick / date-time Goto Github PK
View Code? Open in Web Editor NEWDate and time library for PHP
License: MIT License
Date and time library for PHP
License: MIT License
Hi! First of all thanks for the great work!
Would it be possible to add the following functions to the Duration
class:
toDaysPart()
toHoursPart()
toMinutesPart()
toSecondsPart()
toNanosPart()
They should mimic the behavior of java.time.Duration.
As far as I can see the logic is already implemented in the __toString()
function, so it should be quite easy to extract into functions.
It would be nice if value classes had a support for JSON serialization into ISO strings by implementing JsonSerializable
.
Most, if not all, jsonSerialize
implementations would just be proxies of __toString
.
So my questions are:
Trying to parse the following:
\Brick\DateTime\LocalDate::parse('2024-03-11T00:00:00.000Z');
results in:
Brick\DateTime\Parser\DateTimeParseException Failed to parse "2024-03-11T00:00:00.000Z"
There are some inconsistencies in the API, e.g.
class LocalDate
{
public function getDayOfWeek(): DayOfWeek;
public function getMonth(): int;
}
I understand why we've got getDay(): int
and getYear(): int
-- those values are naturally integer, and java.time makes the same judgement -- but what's the reason for treating getDayOfWeek()
differently to getMonth()
? These are naturally both enums.
Make the TimeZone
parameter optional in the following static factory methods:
LocalDate::now(..)
LocalTime::now(..)
LocalDateTime::now(..)
ZonedDateTime::now(..)
If no time zone is provided, the default one is used. Say:
if ($timeZone === null) {
$timeZone = TimeZone::fromDateTimeZone(new \DateTimeZone());
}
If the proposal is accepted, I can write the changes.
to getStartDate()
/ getEndDate()
. Will require a version bump.
Following code works well (no output):
ZonedDateTime::of(
LocalDateTime::of(2022,1,1),
TimeZoneRegion::parse('Europe/Prague')
);
while this one throws Brick\DateTime\DateTimeException: The time zone offset of 3464 seconds is not a multiple of 60
:
ZonedDateTime::of(
LocalDateTime::of(0,1,1),
TimeZoneRegion::parse('Europe/Prague')
);
The only difference is the year passed into LocalDateTime::of()
. If I try LocalDateTime::min()
instead, it results in the exception as well.
I found out that the border where it stops working is 1891โ1892 (which 130 years back from now). Is this some issue with DateTime
and/or its getOffset()
which is used inside of the ZonedDateTime::of()
?
But it doesn't fail with UTC time zone actually..
In our application, we often times have to create Brick objects (LocalDate
, Duration
, TimeZone
, etc) with code like this :
null !== $var ? Duration::parse($var) : null
It would be nice if methods like parse-rs would allow null
in input, and directly return it if it is null indeed. It would go along with a PHPDoc stating @return ($input is null ? null : self)
, so that static analysers can tell which result it will be. And that way, no more bloated ternaries at call-site.
WDYT ? I'd be willing to work on a pull request in the upcoming two weeks, if that's something you would accept in the library. Let me know!
I'm currently adapting ZonedDateTime
and it would be nice if there was an interface I could use for any future interoperability.
Consider having LocalDateTime and ZonedDateTime extend PHP's DateTimeImmutable for interoperability with API's using PHP's native date objects.
Right now value in Duration object could be negative. (-1 sec)
I don't really get this concept (negative duration, negative distance etc). Could you, please, give some examples when negative Duration is really helps?
var_dump(LocalDateTime::min()->getYear());
var_dump($a= ZonedDateTime::of(LocalDateTime::min(), TimeZone::utc())->getYear());
will yield
-999999
9999
Which is quite unexpected.
Stop me if I'm wrong, but there is no simple, built-in way to do operations like the following in Carbon
:
$nowDate = CarbonImmutable::now();
$previousWeekDate = $nowDate->subWeek();
$startOfPreviousWeekDate = $previousWeekDate ->startOfWeek();
The ThreeTen way is the use of TemporalAdjuster
:
LocalDate nowDate = LocalDate.now();
// Custom temporal adjuster
TemporalAdjuster previousWeekAdjuster = t -> t.minus(Period.ofDays(7));
// Static, utility temporal adjusters
TemporalAdjuster firstMondayAdjuster = TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY);
LocalDate previousWeekDate = nowDate.with(previousWeekAdjuster);
LocalDate startOfPreviousWeekDate = previousWeekDate.with(firstMondayAdjuster);
If it is OK, I would like to investigate and if possible (most likely) implement the ThreeTen way.
Following #48, in addition to LocalDateRange::getDuration
, it would be useful to add a LocalDateRange::toPeriod()
method.
There are several ways to implement this method. Take for example 2021-03-28/2022-04-01
, we can represent it as either:
Not sure which one we should choose here. The first one has a negative part for a non-negative range, so is probably not that intuitive; the third one is probably not what you would expect either, and one may argue that if you just want a raw number of days, you may use getDuration()
instead. So at first glance, I'd go with the second option.
Another solution could be to offer several methods, but this can add to confusion.
I can see that Java's threeten-extra has a toPeriod method, it would be nice to see which way they followed.
Cc @solodkiy
Hi,
I am trying to retrieve an instant from an ISO datetime string, eg "2019-05-10T11:00:00Z".
I have found a way to do this using the IsoParsers::offsetDateTime()
parser but it seems a bit longwinded:
$parseResult = IsoParsers::offsetDateTime()->parse($dateTimeString);
$instant = ZonedDateTime::from($parseResult)->getInstant();
Is this the best way to do this?
Thanks,
Jake
Would you be open to add some Named Constructors just for Convenience?
TimeZone::UTC() === TimeZone::parse('UTC')
TimeZone::EuropeBerlin() === TimeZone::parse('Europe/Berlin')
etc, pp? That way i dont remember which string needs to be passed to parse
At least the on with UTC seems to be very handy?
Hello!
For my project, I made the "week class". It is useful when working on projects with active weeks periods in accounting.
The idea - is not to use time range for week (first day-last day), but use object and store the week in db in one field, as integer week number (1,2 .. 654887). It is very likely as a year has a number (2021 for instance).
A week object inself stores first-last dates, some suitable methods and can be easy calculable: i.e. get a week minus three weeks from the certain week (without manipulation with date/dime objects) ...
I was surprised that nobody find it convenient before me :)
I have a goal to change all of internal date time operations in my project to UTC dates and now looking for the simplest solution. I think that one of approach is to have class which represent such date concept like UTC date time and use it in all internal code and in db. Convert to LocalDateTime and ZonedDateTime only when it relay necessary.
In this lib we have:
What do you think about adding new UtcDateTime class?
UtcDateTime is represent point absolute point in time (sounds similar to Instant, yeah)
And have all methods of LocalDateTime and some more like easy convert to ZonedDateTime(UTC) or Instant.
Or maybe we should just add a simple methods to convert ZonedDateTime to and from UTC LocalDateTime? Something like $zoned->getUtcLocal();
What to you think about it?
In PHP land
In Brick land
Would you be open to swapping the names around to align with PHP? (I can't even extend to rename because the classes are final)
Hello @BenMorel,
As you've noticed, I'm focusing on some performance aspects of this library.
There's one operation we do very often in our codebase : LocalDate::plusDays(1)
. Mostly because we often need to convert "date and time" intervals (with an exclusive end) to "date only" intervals (with an inclusive end), and this is done by calling plusDays(1)
on the end of the interval.
I have a Blackfire inspection that shows that for ~400'000 calls to plusDays()
in a request, it takes 11.5 seconds, of which :
LocalDate::toEpochDay
, of which 2.68 seconds in the method's code itself and 1.25 seconds in intdiv
.LocalDate::ofEpochDay
, of which 4.77 seconds in the method's code itself and 1.92 seconds in intdiv
.I don't think there's much that can be done regarding intdiv
, as it's a native PHP function. And obviously, we can't use a cache to improve performances, as the LocalDate
we call plusDays(1)
from will widely vary (aka represent different dates).
So, I'm left wondering if there's maybe something there that could be done within toEpochDay
and ofEpochDay
themselves ? But looking at the code, I'm quite lost regarding what could actually be improved, except trying to reduce the number of mathematical operations that are done while keeping all the edge-cases they have to deal with...
Or could it be simplified by having a separate method that does specifically plusOneDay
(and not "N" days) ?
I'm eager to read your thoughts and insights about this. Thanks !
gnutix
Hello there,
I'm in the process of upgrading to 0.6, and I've noticed this in the changelog :
YearWeek: the atDay() method now accepts a DayOfWeek instance, passing an integer is deprecated
I'm surprised that the same wasn't done for atMonth
. I found a usage in our code I expected to just work, but I had to add ->value
instead to get the int.
Is it something that was simply forgotten ? Or is there a reason ?
gnutix
We currently have a need for an OffsetDateTime
in java.time parlance. Is there any reason not to add it?
Despite all these time and date classes, it appears there is no method to convert a period to a human-readable format, e.g. 2 years ago. Moreover, it would be beneficial for it to automatically scale, e.g. 2 months instead of 60 days.
To be clear, I am thinking of something akin to moment.duration().humanize().
It may be useful when we want to pass Value Object of TTL seconds. Duration is not the best choice in this situation because TTL cannot contain nanoseconds.
An equals($value) method on all value classes (no type declaration on the parameter) would make this library much easier to work with in combination with other libraries which deal with generic value types, e.g. https://github.com/Space48/auto-value-php.
Since #53, the CI badge on the README is broken: https://github.com/brick/date-time/workflows/CI/badge.svg
This is because we don't have a single CI
workflow anymore, but 3 workflows:
Coding Style
Static Analysis
Tests
Each having its own badge:
@tigitz Do you know of a way to get a single badge for all 3 workflows?
Hi there!
I am trying to determine the Period
between 2 LocalDate
s, where I know I have added a x months. I can't seem to get the result I want
This is best explained with an example:
$start = LocalDate::of(2022, 01, 31);
$end = $start->plusMonths(3);
echo $start, PHP_EOL, $end, PHP_EOL;
echo $period = $start->until($end), PHP_EOL;
2022-01-31
2022-04-30
P2M30D
I want a period of P3M
.
This works fine if the $start
date is not on a complicated end of month day :)
$start = LocalDate::of(2022, 01, 1);
$end = $start->plusMonths(3);
echo $start, PHP_EOL, $end, PHP_EOL;
echo $period = $start->until($end), PHP_EOL;
2022-01-01
2022-04-01
P3M
So,
Many thanks!
Current PatternParser is flexible, but quite excess for tasks like read date from special format (like 'd.m.y')
In php we have special expression language for date formats. (http://php.net/manual/ru/function.date.php)
I am ready to implement parser from this format if you don't have a reason to reject it.
$date = LocalDate::parse($line[3], new PhpFormatParser('d.m.y'));
In ZonedDateTime class we have two methods which looks almost the same, but different in return types:
public function toDateTime() : \DateTime
public function getDateTime() : LocalDateTime
To avoid misunderstanding and confuse I propose rename toDateTime
to toNativeDateTime
.
This approach could be used in other conversions in and from native types. For example:
Period::fromDateInterval
-> Period::fromNativeDateInterval
Period::toDateInterval
-> Period::toNativeDateInterval
LocalDateRange::toDatePeriod
-> LocalDateRange::toNativeDatePeriod
TimeZone::fromDateTimeZone
-> TimeZone::fromNativeDateTimeZone
Another variant is use Php prefix (toDateTime
-> toPhpDateTime
).
Hi,
Just for my own knowledge, why don't you use float to represent the duration ?
For exemple, if I have 5400 seconds, I expect that the toHours()
method returns 1.5.
Thank you in advance for your reply.
According to Wikipedia and moment/moment#3842 (comment) (I don't have access to the norm myself, so I need to quote other sources), the year part of any date in ISO 8601 should have at least four digits, with leading zeros for years < 1000. The PATTERN
constant in YearField
also reflects this.
The new method toISOString()
, however, just does a (string) on the integer member $year
. IMO, this should be handled similar to LocalDate::toISOString()
.
// expected
$until = LocalDate::of(2018, 11, 1);
$today = LocalDate::of(2018, 9, 1);
$period = $today->until($until);
$days = $period->getDays(); // 1
$until = LocalDate::of(2018, 11, 1);
$today = LocalDate::of(2018, 9, 1);
$days = $until->toEpochDay() - $today->toEpochDay(); // 62
I miss any funcion to get real days. It's not possible to get real days from Period.
Hello there,
When we get a date, time, datetime, timezone etc from the database and convert it to a LocalDate
, LocalDateTime
, LocalTime
(etc), we can sometimes safely assume the value is valid (depending on what validation was done when inserting things in the database, aka the write part).
As there's no public constructors on these classes (which I agree is a very good thing), we use *::of()
methods to create the objects. But these have validation checks inside that we can't skip. It might be a good performance optimization when reading a lot of rows from the database that create such objects to be able to skip these checks.
@BenMorel Would you be open to the idea of adding a new argument bool $validate = true
and wrapping those checks in if ($validate)
, so it can be turned off ?
Currently, I have to write code like this to travel forwards by some nanos (to mimic usleep
):
DefaultClock::travel(Instant::now()->plus(Duration::ofNanos($delayNanos)));
It would be nicer if I could write:
DefaultClock::travelForwards(Duration::ofNanos($delayNanos));
Would this be an acceptable addition?
Hello there,
We've started integrating our code a while ago with this package. In some places, we've extended over time the number of classes that are supported as input by a method.
The "worst example" (in the sense: having the greatest number of supported classes) we currently have looks like this :
public function doSomething(
LocalDate|LocalDateTime|LocalDateInterval|LocalDateTimeInterval|YearWeek|YearMonth|Year $temporal,
): void;
There's two types of input here :
brick/date-time
: LocalDate
, LocalDateTime
, YearWeek
, YearMonth
, Year
gammadia/date-time-extra
: LocalDateInterval
, LocalDateTimeInterval
Most of the time, the first thing done in such a method will be something like :
$temporal = LocalDateTimeInterval::cast($temporal);
As everyone of these objects can be represented with a LocalDateTimeInterval, which is the most "precise" of all. This allows to then use complex methods like intersects
and such, which are not necessarily implemented on each of these objects.
It would be nice if the signature of the method could simply be :
public function doSomething(Temporal $temporal): void;
(or TemporalInterval
, or some other name that would represent "any time interval").
Just having an empty* interface implemented on Brick's classes would allow that. Is that something you might be open to ?
I'm very reluctant to make a fork just for a such a simple addition, and not keen on having such complex union types either... (without taking about validation, error messages, etc... everything gets more complex)
I'll gladly submit a PR if you're OK with the idea.
Thanks for your consideration.
gnutix
*: it could also contain some methods / extend other interfaces you're definitely sure you want to have on all these objects, like Stringable
or JsonSerializable
, or even go as far as proposing methods to get time boundaries (think getStart
/ getEnd
) or compare methods (isEqualTo
, isBefore
, isAfter
...) - though it might get complex to get types right...
I'd like to suggest specifying which epoch the library uses as the reference.
Hey there,
i got this:
$whenUtc = ZonedDateTime::parse('2021-07-18T07:00:00Z');
$whenUtc->getTimezone()->isEqualTo(TimeZone::utc()); // -> false
the timezone is then Z
same with etc/UTC which represents the same TimeZone.
Do you want to harmonize those identifiers to represent a Timezone?
e.g.
TimeZone::utc()->isEqualTo(TimeZone::parse('UTC')) // true
would be happy to submit a PR
The value classes should really be final.
Hello!
I just learned about this project from https://www.youtube.com/watch?v=OfY-Sb846ps and it looks very nice.
I don't know if you'd be interested in using static analysis with Psalm or similar tools while developing this. I gave it a go in PR #26. Only a few bits needed changing to make Psalm checks pass at level two.
Hello,
Interval
is a range of two instants and LocalDateRange
is not Zoned nor include time.
However for cases where you need to schedule an event in the future (e.g. from 21/02/2022 10h00 Europe/Paris
to 21/02/2022 12h00 Europe/Paris
) you want to keep the timezone to prevent timezone shifts.
WDYT ?
For consistency with YearMonthRange
, these LocalDateRange
methods should be renamed:
getStartDate()
-> getStart()
getEndDate()
-> getEnd()
It will be useful to have LocalDateTimeRange
to encapsulate start and end datetime. Quite similar to the LocalDateRange
.
Otherwise we need to do something like this if we want to pass range as one parameter.
private function getRegistrationsBetweenDates(
LocalDateRange $dateRange
): array {
$registeredAtFrom = $dateRange->getStart()->atTime(LocalTime::min());
$registeredAtTo = $dateRange->getEnd()->atTime(LocalTime::max());
// ...
}
But this way we can only use 00:00:00 and 23:59:59 time.
Prososal:
private function getRegistrationsBetweenDateTimes(
LocalDateTimeRange $dateTimeRange
): array {
$registeredAtFrom = $dateTimeRange->getStart();
$registeredAtTo = $dateTimeRange->getEnd();
// ...
}
In Java 8 DateTime, the parse
can accept a custom Pattern simply.
LocalDateTime.parse(fields[1].trim(), DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")),
Common task is convert date for human readability format, or some special format like 'd.m.y' and I don't see how I can do this with this class. Did I miss some formatter class or this behavior don't implemented?
Hello there,
I've noticed that YearWeek
and other objects have a __toString()
method returning an ISO representation of the object, but not the corresponding public static function parse(string $iso): self
method. Which leads to multiple usages of *::of()
methods sprouting around in our code (some using substr
, some explode('-W')
, etc... unpredictable code, no proper error management... nasty things).
Is that something you would be willing to accept as a PR?
Thanks for your answer.
gnutix
Hi,
Is there are way to add custom calendars? I know in java this is possible.
If not, well, this is a feature request. ๐
Looks like \DateTimeZone with used in ZonedDateTime::of logic not working correctly with seconds in offset. It silently convert any offset with seconds to UTC timezone.
1) Brick\DateTime\Tests\ZonedDateTimeTest::testOf with data set #17 ('2016-05-17T12:34:56.123456789', '+00:15:01', '+00:15:01', 0, 1463487597, 123456789)
Failed asserting that 1463488496 is identical to 1463487597.
diff --git a/tests/ZonedDateTimeTest.php b/tests/ZonedDateTimeTest.php
index 151b3b4..2bb4edb 100644
--- a/tests/ZonedDateTimeTest.php
+++ b/tests/ZonedDateTimeTest.php
@@ -75,6 +75,7 @@ class ZonedDateTimeTest extends AbstractTestCase
['2014-03-15T12:34:56.123456789', '-00:15', '-00:15', 0, 1394887796, 123456789],
['2015-04-16T12:34:56.123456789', '+00:00', '+00:00', 0, 1429187696, 123456789],
['2016-05-17T12:34:56.123456789', '+00:15', '+00:15', 0, 1463487596, 123456789],
+ ['2016-05-17T12:34:56.123456789', '+00:15:01', '+00:15:01', 0, 1463487597, 123456789],
['2017-06-18T12:34:56.123456789', '+00:30', '+00:30', 0, 1497787496, 123456789],
['2018-07-19T12:34:56.123456789', '+00:45', '+00:45', 0, 1532000996, 123456789],
['2019-08-20T12:34:56.123456789', '+01:00', '+01:00', 0, 1566300896, 123456789],
1) Brick\DateTime\Tests\TimeZoneTest::testFromDateTimeZone with data set #2 ('+01:00:01')
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'+01:00:01'
+'Z'
/home/doc/projects/_forks/date-time/tests/TimeZoneTest.php:81
FAILURES!
Tests: 16, Assertions: 23, Failures: 1.
[doc@doc-pc ~/projects/_forks/date-time (master)]# git diff tests/Time
TimeZoneOffsetTest.php TimeZoneRegionTest.php TimeZoneTest.php
[doc@doc-pc ~/projects/_forks/date-time (master)]# git diff tests/TimeZoneTest.php
diff --git a/tests/TimeZoneTest.php b/tests/TimeZoneTest.php
index 4e4709f..60b0174 100644
--- a/tests/TimeZoneTest.php
+++ b/tests/TimeZoneTest.php
@@ -86,6 +86,7 @@ class TimeZoneTest extends AbstractTestCase
return [
['Z'],
['+01:00'],
+ ['+01:00:01'],
['Europe/London'],
['America/Los_Angeles']
];
I suggest Interval
should have the following methods, like in LocalDateRange
:
contains(Instant): bool
intersectsWith(Interval): bool
getIntersectionWith(Interval): Interval
of(Instant, Instant): self
- for consistency with other classesand nice to haves (also contained in the LocalDateRange
):
isEqualTo(Interval): bool
toNativeDatePeriod(): \DatePeriod
http://php.net/manual/en/language.oop5.late-static-bindings.php
Is it possible to use this approach, so if one subclassed your class, constructors are returning also instances of the subclass instead of the original one? Otherwise, subclassing has limited usage.
Example: I wanted to subclass LocalDate to add a very simple "format" method, that will just return "YYYY-mm-dd", but there might be other use cases.
This #45 (comment) made be rethink of whether the current behaviour of LocalDateRange
(start inclusive, end inclusive) is the correct one.
It was correct for my use case when I designed this class, but I realize that it may not be the same for everyone, and that it may depend on the use case. For consistency with other classes such as Interval
, we could say that it should be exclusive of the end, but at the same time, I see a range of dates as a different beast from a range of date-times: in a calendar, if you say "from 2023-01-03 to 2023-01-10", I'd expect the 10th to be part of the range.
So I checked what's been done in threeten-extra in Java, and they actually offer both ways:
static LocalDateRange of(LocalDate startInclusive, LocalDate endExclusive);
static LocalDateRange ofClosed(LocalDate startInclusive, LocalDate endInclusive);
LocalDate getStart();
LocalDate getEnd(); // exclusive
LocalDate getEndInclusive();
I like the configurability of this behaviour, as it covers pretty much all use cases. Internally, we can choose either, as it doesn't matter to the outside world.
On the other hand, I'm personally not fond of the fact that of()
and getEnd()
are implicitly exclusive, and that you need to use ofClosed()
/ getEndInclusive()
to make them inclusive.
Therefore I'm thinking of making everything explicit, which leaves no doubt but is a little ugly:
static function ofEndExclusive(LocalDate startInclusive, LocalDate endExclusive): LocalDateRange;
static function ofEndInclusive(LocalDate startInclusive, LocalDate endInclusive): LocalDateRange;
function getStart(): LocalDate;
function getEndExclusive(): LocalDate;
function getEndInclusive(): LocalDate;
Notes:
ofEndExclusive()
, but at least it's clear on the intent, rather than ofHalfOpen()
for example; suggestions welcomegetStart()
be called getStartInclusive()
for consistency? It's going a bit far IMO, as there are no classes that offer a start exclusive (I don't think there are many use cases for this)Alternatively, just copying the public API of threeten-extra as-is has a couple of advantages:
Interval
(and possibly incoming LocalDateTimeRange and ZonedDateTimeRange) are all exclusive of the end, if could make sense to be exclusive by default, and that the inclusive ones have to be explicitly suffixed.I'm still a bit worried by people misusing it by believing it's end-inclusive by default, but the parameter names can help I guess.
Final thought: changing the default behaviour of of()
/ getEnd()
is a quite heavy BC break, for which we can't trigger a deprecation notice.
What do you think, @antonkomarev, @solodkiy, @gnutix, @tigitz, @joshdifabio?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.