-
Notifications
You must be signed in to change notification settings - Fork 52
A reorganization of timekeeping to work with general calendars #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This branch needs to be rebased after #86 gets merged. |
TestingI tested on my laptop using a G-240km test case (and skipping I ran the test cases pointed to by both |
This branch implements the design in #103 |
@xylar, I'll be happy to do a review following the rebase to at least give some comments. |
@pwolfram, when you get around to reviewing this PR, please take a look at the CI I added for testing my new MpasRelativeDelta class near leap days. Feel free to suggest other tests I should add if you can think of any. |
return self.__neg__().__radd__(other) | ||
|
||
def __sub__(self, other): | ||
return NotImplemented |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why subtraction isn't supported but addition is supported? You also have negation, which suggests subtraction should work as a combination of addition and negation (or multiplication with a negative factor), e.g., A - B = A + (-B). This should be commented or implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that's because date - offset is sensible but offset - date doesn't really make sense. So __rsub__
is okay but __sub__
should be undefined. Have I missed something? Keep in mind that addition and subtraction is with datetime.datetime objects, not with other MpasRelativeDelta
objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would argue that the subtraction operation needs to be commutative so that offset - date should make sense because it is just offset - date = - (date - offset)
. This is because the negation operator is defined and I'm using standard arithmetic set operations, which I think make sense in this context and is presumed by many. Alternatively, we should issue a warning that this is not implemented, which is true in general for operators like this that seem like they should intuitively be implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Negation operator on a date doesn't make sense and puts a date out of range. So I don't think either offset - date
or - (date - offset)
are supported. I don't believe datetime.datetime
supports negation, and I think that is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I would say that offset1 - offset2 = - (offset2 - offset1)
if you're using two MpasRelativeDeltas but if you're using datetime.datetime
as one of the objects, that's no longer necessarily a sensible operator.
Only relative intervals (years, months, etc.) are supported and not the | ||
absolute date specifications (year, month, etc.). Addition/subtraction | ||
of datetime.datetime objects (but not other MpasRelativeDelta, | ||
datetime.timedelta or other related objects) is supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this the case? Additional comments are needed or this restriction should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we have a use case where we need to be able to add or subtract two offsets. I'm not sure if it's allowed for timedeltas. I thought not but I'll check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, it looks like timedelta supports addition and subtraction of other timedeltas so I'll implement it. It doesn't seem that hard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should test all overloaded operator operations too because this is best practices and will ensure that we get what is expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe I'm testing them all at this point.
""" | ||
Temporary function for adding an offset year and clamping a datetime to | ||
range supported by `numpy.datetime64`. | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this issue a warning (perhaps that can be turned off via a flag) that specifies which direction the clamp, i.e., floor
or ceil
, was performed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I had misread your comment the first time. There are a lot of cases where this function is used precisely because the clamping behavior is desired, so it would be important to suppress the warning in those cases. I'm inclined not to add the complexity required to have an optional warning, given that this function really is temporary and the next PR I am planning to make will eliminate this function entirely, along with yearOffset. But I can add this functionality if you feel strongly about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going to get rid of this on a short timescale of weeks, but not months, this is noise and we should proceed in the less rigorous way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's my intention to have this replaced as soon as possible (definitely weeks rather than months). That partly depends on the speed of reviews and testing of the PRs that come before it. So I appreciate you working on this.
self.assertEqual(date1, date2) | ||
|
||
# gregorian | ||
calendar = 'gregorian' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about looping over gregorian
and gregorian_noleap
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I'll do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to test the whole range of acceptable dates for both and then the distinction between gregorian
and gregorian_noleap
for each distinguishing case, e.g., Feb 29.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? We can do that, but that seems pretty extreme to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I don't really understand what you mean by "test the whole range of acceptable dates"? I guess you mean the different string formattings? I think it's pretty redundant to check this for both calendars because the function for parsing these strings is completely independent of the calendar, it just keeps the calendar around for doing math operations later on. So in any cases where we're not doing math, the calendar is irrelevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I'm looping over both calendars anyway. So I guess this is resolved.
date2 = datetime.datetime(year=2262, month=1, day=1) | ||
self.assertEqual(date1, date2) | ||
|
||
# test if the calendars behave as they should |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop could go here and reduce the number of lines of code required by about a factor of two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind doing this and I agree it makes sense to systematically test both calendars but there will be no large reduction in code. Each test is unique (except for the first 2) independent of calendar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, this makes sense and will clarify the code albeit not at a huge reduction in the amount of code. It will be clearer that this set of cases responds the same for each calendar and this set responds differently. Perhaps you could do something like I'll show you below in another comment... to make the distinction clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I think this is addressed.
calendar='gregorian') | ||
diff = date1-date2 | ||
self.assertEqual(diff, stringToDatetime('1995-12-26')) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see some tests on operators for MpasRelativeDelta
instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of these tests already do test addition and subtraction operators. I will add some more systematic tests of all operators, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be good. But you could also note that a particular test is doing both in a comment too if that is cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I've done so.
@xylar, I added some incomplete and preliminary comments on this PR to get you started. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some preliminary comments related to time-based classes and testing. I suspect there may be some more comments following the rebase and further understanding of additional challenges with calendars.
self.assertEqual(stringToDatetime('2016-03-01') - | ||
stringToRelativedelta('0000-00-01', | ||
calendar='gregorian'), | ||
stringToDatetime('2016-02-29')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about simplifying via something like:
for calendar, expected in zip(['gregorian', 'gregorian_noleap'], ['2016-03-01', '2016-02-29']):
self.assertEqual(stringToDateTime('2016-03-01') - stringToRelativedelta('0000-00-01', calendar=calendar),
stringToDateTime(expected))
This more clearly shows the distinction between the calendars and their usage and reduces the amount of needed code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I'll do that.
c57ef11
to
31e8742
Compare
31e8742
to
9178c1c
Compare
@pwolfram and @milenaveneziani, this PR has been rebased onto develop and is ready to review. I have left on the "Don't Merge" flag because I want to squash some commits before it gets merged. |
9178c1c
to
c22e6c8
Compare
Remove Date class. Update streams to use new timekeeping utility. Update all analysis scripts to use timekeeping utility. Update testing to new timekeeping utility funs.
c22e6c8
to
a44e572
Compare
Okay, I have tested this PR on Edison (while /project is still available!) with the usual beta0 run (in configs/edison) and results are bit-for-bit to those from I squashed commits, so the PR is now ready to merge. @pwolfram, please let me know if there are remaining concerns that I haven't addressed. @milenaveneziani, please test and merge once you get the go-ahead from @pwolfram. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few extremely minor comments and this is great forward progress and I'm signing-off-- LGTM.
__truediv__ = __div__ | ||
|
||
def __repr__(self): | ||
l = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous for multiple reasons-- I would greatly prefer that we never use single-character variables unless they are an index. Furthermore, "l" looks a lot like a "1", especially in certain fonts. Cna you please make this something like rep
or strrep
, e.g. something three characters or more instead of l
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I copied this code straight out of relativedelta
and only modified the minimum to add the calendar. I agree that it's badly written. I'll clean it up.
Last modified | ||
------------- | ||
02/04/2017 | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An aside-- we should set something up that produces a "readthedocs" descrition of at least our public API. Feel free to do the honors for an issue or I can also do so. I'm happy to set this up when I get a a moment too if you would like so that we can leverage all your really good work getting docstrings into a more general sphinx-compatable format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Go for it. I agree that this needs to happen but I haven't had time (and won't for awhile) to look into it further.
# YYYY-MM-DD | ||
# SSSSS | ||
|
||
for calendar in ['gregorian', 'gregorian_noleap']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Thanks @xylar :-)
date2 = datetime.datetime(year=2262, month=1, day=1) | ||
self.assertEqual(date1, date2) | ||
|
||
def test_MpasRelativeDeltaOps(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
|
||
# make sure there's an error when we try to add MpasRelativeDeltas | ||
# with different calendars | ||
with self.assertRaisesRegexp(ValueError, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, because this enforces a design intent.
This merge adds a new MpasRelativeDelta class derived from dateutils.relativedelta.relativedelta. This class extends relativedelta to handle both MPAS calendars (important for addition to/subtraction from datetime.datetime objects). The timekeeping tests have been updated to work with the new class and to include new tests that ensure the right behavior for manipulating dates near leap days.
a44e572
to
f5382cd
Compare
@pwolfram, I think I've addressed your remaining comment. |
@xylar, I'm giving the go-ahead to @milenaveneziani for the merge. |
The Date class has been replaced by a few utility functions:
stringToDatetime
- converts an MPAS date string to adatetime.datetime
stringToRelativeDelta
- converts an MPAS date string to anMpasRelativeDelta
clampToNumpyDatetime64
- clamps a datetime to the valid range supported by xarrayA new
MpasRelativeDelta
class has been added (deriving fromdateutil.relativedelta.relativedelta
)All analysis functions and tests have been updated to work with these utility functions.