Skip to content

Feat: construct datetime from record#15455

Merged
sholderbach merged 33 commits intonushell:mainfrom
LoicRiegel:fix/construct-datetime-from-record
Apr 10, 2025
Merged

Feat: construct datetime from record#15455
sholderbach merged 33 commits intonushell:mainfrom
LoicRiegel:fix/construct-datetime-from-record

Conversation

@LoicRiegel
Copy link
Copy Markdown
Contributor

@LoicRiegel LoicRiegel commented Mar 30, 2025

Issue #12289, can be closed when this is merged

Description

Currently, the into datetime command's signature indicates that it supports input as record, but it was actually not supported.

This PR implements this feature.

User-Facing Changes

into datetime's signature changed (see comments)

Happy paths

Note: I'm in +02:00 timezone.

> date now | into record | into datetime
Fri, 4 Apr 2025 18:32:34 +0200 (now)

> {year: 2025, month: 12, day: 6, second: 59} | into datetime | into record
╭─────────────┬────────╮
 year         2025   
 month        12     
 day          6      
 hour         0      
 minute       0      
 second       59     
 millisecond  0      
 microsecond  0      
 nanosecond   0      
 timezone     +02:00 
╰─────────────┴────────╯

> {day: 6, second: 59, timezone: '-06:00'} | into datetime | into record
╭─────────────┬────────╮
 year         2025   
 month        4      
 day          6      
 hour         0      
 minute       0      
 second       59     
 millisecond  0      
 microsecond  0      
 nanosecond   0      
 timezone     -06:00 
╰─────────────┴────────╯

Edge cases

{} | into datetime
Fri, 4 Apr 2025 18:35:19 +0200 (now)

Error paths

  • A key has a wrong type
    > {month: 12, year: '2023'} | into datetime
    Error: nu::shell::only_supports_this_input_type
    
      × Input type not supported.
      ╭─[entry #8:1:19]
    1  {month: 12, year: '2023'} | into datetime
      ·                   ───┬──    ──────┬──────
      ·                                  ╰── only int input data is supported
      ·                      ╰── input type: string
      ╰────
    > {month: 12, year: 2023, timezone: 100} | into datetime
    Error: nu::shell::only_supports_this_input_type
    
      × Input type not supported.
      ╭─[entry #10:1:35]
    1  {month: 12, year: 2023, timezone: 100} | into datetime
      ·                                   ─┬─    ──────┬──────
      ·                                               ╰── only string input data is supported
      ·                                    ╰── input type: int
      ╰────
  • Key has the right type but value invalid (e.g. month=13, or day=0)
    > {month: 13, year: 2023} | into datetime
    Error: nu::shell::incorrect_value
    
      × Incorrect value.
      ╭─[entry #9:1:1]
    1  {month: 13, year: 2023} | into datetime
      · ───────────┬───────────   ──────┬──────
      ·                                ╰── one of more values are incorrect and do not represent valid date
      ·            ╰── encountered here
      ╰────
    > {hour: 1, minute: 1, second: 70} | into datetime
    Error: nu::shell::incorrect_value
    
      × Incorrect value.
       ╭─[entry #3:1:1]
     1  {hour: 1, minute: 1, second: 70} | into datetime
       · ────────────────┬───────────────   ──────┬──────
       ·                                         ╰── one of more values are incorrect and do not represent valid time
       ·                 ╰── encountered here
       ╰────
  • Timezone has right type but is invalid
    > {month: 12, year: 2023, timezone: "+100:00"} | into datetime
    Error: nu::shell::incorrect_value
    
      × Incorrect value.
      ╭─[entry #11:1:35]
    1  {month: 12, year: 2023, timezone: "+100:00"} | into datetime
      ·                                   ────┬────    ──────┬──────
      ·                                                     ╰── encountered here
      ·                                       ╰── invalid timezone
      ╰────
  • Record contains an invalid key
    > {month: 12, year: 2023, unknown: 1} | into datetime
    Error: nu::shell::unsupported_input
    
      × Unsupported input
      ╭─[entry #12:1:1]
    1  {month: 12, year: 2023, unknown: 1} | into datetime
      · ─────────────────┬─────────────────   ──────┬──────
      ·                                            ╰── Column 'unknown' is not valid for a structured datetime. Allowed columns are: year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timezone
      ·                  ╰── value originates from here
      ╰────
  • If several issues are present, the user can get the error msg for only one, though
    > {month: 20, year: '2023'} | into datetime
    Error: nu::shell::only_supports_this_input_type
    
      × Input type not supported.
      ╭─[entry #7:1:19]
    1  {month: 20, year: '2023'} | into datetime
      ·                   ───┬──    ──────┬──────
      ·                                  ╰── only int input data is supported
      ·                      ╰── input type: string
      

Tests + Formatting

Tests added
Fmt + clippy OK

After Submitting

Maybe indicate that in the release notes
I added an example in the command, so the documentation will be automatically updated.

@LoicRiegel LoicRiegel force-pushed the fix/construct-datetime-from-record branch from f069368 to 95fe428 Compare March 30, 2025 17:13
@sholderbach
Copy link
Copy Markdown
Member

The existing record -> record signature is functional to map particular fields of a record to datetime.

{a: (date now | into int), b: blub} | into datetime a

I think we never added it because there will be a type inference challenge

Given a record input type it will be undecidable what the output type will be.

record -> record
record -> datetime

@Bahex
Copy link
Copy Markdown
Member

Bahex commented Apr 1, 2025

@sholderbach I would be in favor of removing this field-mapping behavior since it's simple to use update for both record -> record and table -> table conversions.

@Bahex
Copy link
Copy Markdown
Member

Bahex commented Apr 1, 2025

@LoicRiegel First of all, thanks for implementing this!

In previous discussions about this conversion on discord we ended up with some interesting semantics regarding missing fields.

  • timezone is assumed to be local, unless provided in the record
  • empty fields are filled in a specific way, the the time units bigger than the biggest provided fields are assumed to be current and smaller ones are zeroed:
  • {day: 4} | date from-record has the current year and month, hour minute etc are zero
  • {hour: 5} | date from-record is today 05:00
  • {day: 2, minute: 4} is the 2nd day of current month, first day, 00:04
  • into record | into datetime result is identical to its input

Can you implement this behavior? You can use my half-baked implementation as reference.

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

The existing record -> record signature is functional to map particular fields of a record to datetime.

{a: (date now | into int), b: blub} | into datetime a

Is it functional, thoug?

> {a: (date now | into int), b: blub} | into datetime a
╭───┬──────╮
 a  now  
 b  blub 
╰───┴──────╯
> {a: (date now | into int), b: blub} | into datetime a | describe
record<a: date, b: string>

@sholderbach I would be in favor of removing this field-mapping behavior since it's simple to use update for both record -> record and table -> table conversions.

I would also be in favor of removing that, especially if it has never worked, but also this would make the signature clearer.

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

@LoicRiegel First of all, thanks for implementing this!

In previous discussions about this conversion on discord we ended up with some interesting semantics regarding missing fields.

  • timezone is assumed to be local, unless provided in the record
  • empty fields are filled in a specific way, the the time units bigger than the biggest provided fields are assumed to be current and smaller ones are zeroed:
  • {day: 4} | date from-record has the current year and month, hour minute etc are zero
  • {hour: 5} | date from-record is today 05:00
  • {day: 2, minute: 4} is the 2nd day of current month, first day, 00:04
  • into record | into datetime result is identical to its input

Can you implement this behavior? You can use my half-baked implementation as reference.

Thank you :)
Yes I can implement those extra requirements, but a few questions:

  • basically that means that you take "today" and local timezone of the user as the default, but that implies that the commands will have different outputs depending on where you are and what time it is. Wouldn't you expect from a command like this to be deterministic, always produce the same output for the same input?
  • Could we implement 2 strategies: (a) default everything as 0, (b) default everything using current location and date time. I could imagine something like $rec | into datetime -z LOCAL runs strategy (b) and $rec | into datetime uses strategy (a) by default, although this thing in particular is not good enough IMO
  • Those assumptions should be documented somewhere: I feel like close to the code would be nice.

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

I'm thinking, if users want to use his/her current location and datetime, something like this would work

> date now | into record | update hour 5 | into datetime
Tue, 1 Apr 2025 05:54:54 +0200 (9 hours ago)

@Bahex
Copy link
Copy Markdown
Member

Bahex commented Apr 1, 2025

  • basically that means that you take "today" and local timezone of the user as the default, but that implies that the commands will have different outputs depending on where you are and what time it is. Wouldn't you expect from a command like this to be deterministic, always produce the same output for the same input?

into datetime already uses the local timezone (except with unix timestamps) and fills in the larger time units with the current date.

> "5:00" | into datetime | to nuon
2025-04-01T05:00:00+03:00

It makes sense to me to have the same behavior for records.

> {hour: 5} | into datetime | to nuon
2025-04-01T05:00:00+03:00

As for deterministic behavior, because the fields smaller than the largest specified field default to 0, specifying the year and the timezone in the input makes the output deterministic.

> {year: 2025, month: 12, day: 6, hour: 16, minute: 0, second: 59, timezone: "+00:00"} | into datetime | to nuon
2025-12-06T16:00:59+00:00
  • Could we implement 2 strategies: (a) default everything as 0, (b) default everything using current location and date time. I could imagine something like $rec | into datetime -z LOCAL runs strategy (b) and $rec | into datetime uses strategy (a) by default, although this thing in particular is not good enough IMO

I don't think specifying the timezone should affect the whether the current time or 0 is used. If such an option is necessary, I would rather have it as a separate flag.

  • Those assumptions should be documented somewhere: I feel like close to the code would be nice.

Agreed. Adding a few more examples that demonstrate this behavior would be nice too.

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

into datetime already uses the local timezone (except with unix timestamps) and fills in the larger time units with the current date.

> "5:00" | into datetime | to nuon
2025-04-01T05:00:00+03:00

It makes sense to me to have the same behavior for records.

Oh I didn't realize that! In that case, I totally agree with you, we should definitely have the same behavior.

As for deterministic behavior, because the fields smaller than the largest specified field default to 0, specifying the year and the timezone in the input makes the output deterministic.

Okay fair enough

I don't think specifying the timezone should affect the whether the current time or 0 is used. If such an option is necessary, I would rather have it as a separate flag.

Yeah absolutely; forget everything I said about the possible strategies

Agreed. Adding a few more examples that demonstrate this behavior would be nice too.

Ok for a couple of examples, and maybe I'll check the documentation website.

@sholderbach
Copy link
Copy Markdown
Member

Is it functional, thoug?

> {a: (date now | into int), b: blub} | into datetime a
╭───┬──────╮
 a  now  
 b  blub 
╰───┴──────╯
> {a: (date now | into int), b: blub} | into datetime a | describe
record<a: date, b: string>

Your example show that it is in fact functional in that it converts the scalar values in the given columns.

I put the topic of this behavior on the meetings agenda for tomorrow. We have a ton of commands that accept optional columns (and thus operate on whole tables or records on the given columns)

The alternative for multiple columns is pretty wordy

Either multiple update $col {command} pipeline steps or the wordy invocation of update cells --columns $cols {command}

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

Your example show that it is in fact functional in that it converts the scalar values in the given columns.

You're right, I don't know what I what I was thinking. I guess I was visually expecting the full string describing the date, and not just "now" in the table.

@sholderbach
Copy link
Copy Markdown
Member

I am gonna mark this as draft until we have a recommendation from the meeting.

Otherwise merging this could lead to typecheck bugs in the following situation

produces-record | into datetime $col | expects-record
produces-record | into datetime | expects-datetime

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

LoicRiegel commented Apr 3, 2025

Update after I worked on it tonight

Done:

  • modified the signature to add record -> any as discussed yesterday. I also left both record -> record and record -> datetime
  • empty fields are filled in a specific way, the the time units bigger than the biggest provided fields are assumed to be current and smaller ones are zeroed

BTW the code grew quite a bit, I'm happy to take any improvement suggestions:)

Left to do:

  • timezone is assumed to be local, unless provided in the record

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

LoicRiegel commented Apr 4, 2025

Okay, I just pushed the last commits, so now we take the local timezone if the record does not contain it.

I updated the examples in the PR description

The PR is ready for review again :)

@LoicRiegel LoicRiegel marked this pull request as ready for review April 4, 2025 16:38
@LoicRiegel LoicRiegel force-pushed the fix/construct-datetime-from-record branch from 89fc02a to 40f2840 Compare April 7, 2025 20:35
@LoicRiegel
Copy link
Copy Markdown
Contributor Author

I just rebased to resolve the conflicts

@LoicRiegel LoicRiegel requested a review from sholderbach April 8, 2025 21:59
Copy link
Copy Markdown
Member

@sholderbach sholderbach left a comment

Choose a reason for hiding this comment

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

Thanks for the thorough testing including the error paths and your extensive description of those in the PR note.

}
}

fn merge_record(record: &Record, head: Span, span: Span) -> Value {
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.

Minor thought, if this were -> Result<Value, ShellError> the internal code could simplfy quite a bit with ? and .map_err, at the cost of having to unify that for the fn action() signature later.

But that is not critical for the funciton.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay, thanks for the tip!

I wanted to implement the construct from record for into duration too, so maybe I'll have a look then.

@sholderbach sholderbach merged commit dfca117 into nushell:main Apr 10, 2025
15 checks passed
@sholderbach sholderbach added the notes:mention Include the release notes summary in the "Hall of Fame" section label Apr 10, 2025
@github-actions github-actions bot added this to the v0.104.0 milestone Apr 10, 2025
@sholderbach sholderbach added the A:datetime-handling Semantics and implementation of the datetime/duration types and commands label Apr 10, 2025
@LoicRiegel LoicRiegel deleted the fix/construct-datetime-from-record branch April 10, 2025 14:57
@LoicRiegel
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough testing including the error paths and your extensive description of those in the PR note.

You're welcome! It's worth it 😄

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

I'll also write something down in the release notes about this one

@LoicRiegel
Copy link
Copy Markdown
Contributor Author

Issue #12289, can be closed when this is merged

Also could you please close the issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A:datetime-handling Semantics and implementation of the datetime/duration types and commands notes:mention Include the release notes summary in the "Hall of Fame" section

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants