ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

ARROW-13549: [C++] Add casts from timestamp to date/time - #10933

Closed
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549
Closed

ARROW-13549: [C++] Add casts from timestamp to date/time#10933
lidavidm wants to merge 2 commits into
apache:masterfrom
lidavidm:arrow-13549

Conversation

@lidavidm

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

@rok

rok commented Aug 15, 2021

Copy link
Copy Markdown
Member

Looks like this will conflict with #10457/ARROW-12980 so we may want to hold off on this one, as that one looks close.

Indeed. Looks great! The only thing I would recommend would be to use MakeTemporal from ARROW-12980 instead of MakeTimeTemporal.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

ARROW-12980 looks like it should be close so I'll rebase on top of that now.

@rok

rok commented Aug 17, 2021

Copy link
Copy Markdown
Member

@lidavidmARROW-12980 was merged.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Thanks for the heads up! I'll get this rebased soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ARROW-12980, this now works with timezones? (or can work)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I forgot to update the docstring. Yes, they all work with timezones and there are tests. I've updated all the docstrings.

@pitrou

Copy link
Copy Markdown
Member

Why aren't these implemented as cast kernels instead?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Ah, that makes sense. I'll update this to be a cast instead, though I think we are going to need some refactoring of the utilities.

@lidavidm
lidavidm marked this pull request as draft August 18, 2021 16:14
@lidavidm

Copy link
Copy Markdown
MemberAuthor

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

>>> timestamps
<pyarrow.lib.TimestampArray object at 0x7f676cd86fa0>
[
1970-01-01 00:00:59.123456789,
2000-02-29 23:23:23.999999999,
1899-01-01 00:59:20.001001001
]
>>> timestamps.cast(pa.date64(), safe=False)
<pyarrow.lib.Date64Array object at 0x7f676cf277c0>
[
1970-01-01,
2000-02-29,
1899-01-02
]
>>> pc.date64(timestamps)
<pyarrow.lib.Date64Array object at 0x7f676cf27d00>
[
1970-01-01,
2000-02-29,
1899-01-01
]

Also Python doesn't expose a way to set only allow_time_truncate (though maybe we should just allow the cast if we refactor things here). But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion). Also for instance a cast of a timestamp-with-timezone right now is quite different than what this does.

@rok

rok commented Aug 18, 2021

Copy link
Copy Markdown
Member

Date extraction could also be thought of as a rounding to a day interval.

@jorisvandenbossche

Copy link
Copy Markdown
Member

Personally, I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have.
As @lidavidm also mentions, this is in general "unsafe" cast, but when you explicitly want to extract a time/date component, it is obvious you want this and having to allow an unsafe cast feels unnecessarily.

There's already a cast from timestamp to date32/date64, however, placing this implementation there would change semantics a little bit:

I would say that the current casting semantics are wrong and should be fixed? (in any case, the "extracted" date is clearly wrong, whether that's seen as a consequence of the "unsafe" cast or not is to be discussed I suppose)

There are actually two "unsafe" steps in this conversion it seems (which explains the wrong part):

arr=pa.array(["1970-01-01 00:00:59.123456789","2000-02-29 23:23:23.999999999","1899-01-01 00:59:20.001001001"]).cast(pa.timestamp("ns"))
>>>arr.cast(pa.date64())
...
ArrowInvalid: Castingfromtimestamp[ns] todate64[ms] wouldlosedata: 59123456789
../src/arrow/compute/kernels/scalar_cast_temporal.cc:178 (ShiftTime<int64_t, int64_t>(ctx, conversion.first, conversion.second, input, output))
# that error is actually coming from a conversion to milliseconds# (and you don't really care about the part being lost for conversion to date ..)>>>arr.cast(pa.timestamp("ms"))
...
ArrowInvalid: Castingfromtimestamp[ns] totimestamp[ms] wouldlosedata: 59123456789# when ignoring this lost part in conversion to ms, then casting to date gives another error:>>>arr.cast(pa.timestamp("ms"), safe=False).cast(pa.date64())
...
ArrowInvalid: Timestampvaluehadnon-zerointradaymilliseconds

And I suppose that when those intraday milliseconds are ignored by doing safe=False, we do a simple round to get rid of those:

constint64_t remainder = out_data[i] % kMillisecondsInDay;
if (ARROW_PREDICT_FALSE(!options.allow_time_truncate && remainder > 0)) {
returnStatus::Invalid("Timestamp value had non-zero intraday milliseconds");
}
out_data[i] -= remainder;

By subtracting the remainder we basically round towards zero (it seems C++ module operator behaves differently as Python when involving negative integers -12 % 10 = -2 in C++ and -12 % 10 = 8 in Python), which means that for negative values we are rounding up, and we should round down instead? (that could be seen as a bug fix?)

@pitrou

Copy link
Copy Markdown
Member

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

@pitrou

Copy link
Copy Markdown
Member

I think having a separate (non-cast) kernel to extract those components make sense from a user perspective (but can of course share implementation), and complementing the other timestamp component extraction kernels we already have

My problem is that I don't even understand the difference they're supposed to make to the "normal" casts. Are those (supposedly) different semantics really desired?

I would favour fixing/improving the currently implemented casts, if necessary.

@lidavidm

Copy link
Copy Markdown
MemberAuthor

But that does raise the question of whether a cast is appropriate for this operation (since it seems like casting is generally interpreted more like reinterpret_cast, while this is an actual conversion).

I'm not sure I understand what you mean with the reinterpret_cast comment. Our casts are definitely conversions (see the Decimal -> Decimal casts for example).

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think the path here is to make the kernels in this PR into safe casts, so that users don't have to specify an unsafe cast. (In theory you could get away with just allow_time_truncate but I think there's no way to pass that in Python.)

@lidavidm
lidavidm marked this pull request as ready for review August 23, 2021 13:29
@lidavidm
lidavidm marked this pull request as draft September 9, 2021 12:33
@lidavidm
lidavidmforce-pushed the arrow-13549 branch 2 times, most recently from 2b9b2a2 to bda1eaeCompareSeptember 9, 2021 13:29
@lidavidm
lidavidm marked this pull request as ready for review September 9, 2021 13:31
@lidavidm

Copy link
Copy Markdown
MemberAuthor

This is now implemented as a cast and is rebased. For casting timestamp->time, we do check the truncation/overflow flags in some scenarios (e.g. if you want to cast a nanosecond timestamp to a time32).

@jorisvandenbossche

Copy link
Copy Markdown
Member

BTW, I stumbled on https://issues.apache.org/jira/browse/ARROW-10213, so it seems @lidavidm you already opened an issue about this buggy (round instead of extract, see above #10933 (comment)) behaviour a while ago .. :-)
So I think this PR is now closing that issue as well?

@lidavidm

Copy link
Copy Markdown
MemberAuthor

Whoops! Yeah, let me link/close-as-duplicate the issues and update the description. Thanks for finding this.

@jorisvandenbossche

Copy link
Copy Markdown
Member

I guess I was wondering if the current behavior (just 'reinterpreting' the timestamp) is still useful, it sounds like not.

I think there can be some value in the current meaning of "safe" cast of timestamp to date. For example, it would allow you to convert timestamps-which-are-actually-dates safely to dates, without loosing any time information. While if we make the safe cast to ignore the time values by default, the only way to do this is by first checking if all hour/minute/second/subsecond components are zero.
In the end, this is very similar to our casting rule of floats to ints: by default a safe cast only allows it for "round" floats without decimals, and forcing an unsafe cast actually rounds / discards decimals).

@lidavidmlidavidm changed the title ARROW-13549: [C++] Add date/time extraction functionsARROW-13549: [C++] Add casts from timestamp to date/timeSep 16, 2021
@lidavidm

Copy link
Copy Markdown
MemberAuthor

I've rebased this again.

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one suggestion

}

template <typename T>
enable_if_timestamp<T, const std::string> GetInputTimezone(const DataType& type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this conflict with the non-template GetInputTimezone(const DataType&) above? At least it seems there's a potential for confusion. Perhaps we can simply reconcile both implementations? For example:

staticinlineconst std::string& GetInputTimezone(const DataType& type) {
staticconst std::string no_timezone = "";
switch (type.id()) {
case Type::TIMESTAMP:
return checked_cast<const TimestampType&>(type).timezone();
default:
return no_timezone;
}
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed. I suppose it worked before since it was called as GetInputTimezone<T>(...).

@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Any further comments on this?

ViniciusSouzaRoque pushed a commit to s1mbi0se/arrow that referenced this pull request Oct 20, 2021
Closesapache#10933 from lidavidm/arrow-13549
Authored-by: David Li <li.davidm96@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@lidavidm@rok@pitrou@jorisvandenbossche