Powershell: A simple method for creating TimeSpan objects to enable more user-friendly and simple usage of time-dependent Cmdlets

Created on 12 Apr 2020  路  52Comments  路  Source: PowerShell/PowerShell

Summary of the new feature/enhancement

Motivation

Cmdlets such as Start-Sleep offer both -Seconds and -Milliseconds as parameters.
This style of time input is functional, but has a few issues:

  • Expanding the number of units requires updating the argument handling and the function body itself
  • Implementation in custom scripts or cmdlets requires boilerplate
  • Available options may not be consistent across cmdlets

Proposed solution

A new syntax expansion that adds support for TimeSpan literals, and cmdlets such as Start-Sleep accepting TimeSpan objects by default.
This entails adding new suffixes to numeric literals as described here

Preliminary list of suffixes:

  • min minutes
  • sec seconds
  • ms milliseconds
  • tick ticks
  • h hours
  • day days
  • year years (365 days, notably debatable due to calendar and culture issues)

Examples:

Items in each line are equivalent:

10sec = New-TimeSpan -Seconds 10 = [TimeSpan]100000000
15min = New-TimeSpan -Minutes 15 = [TimeSpan]9000000000
0xFFyear = New-TimeSpan -Days (0xFF * 365) = [TimeSpan]80416800000000000

Advantage over New-TimeSpan

This solution is significantly more concise than using New-TimeSpan (see above).
The semantics of this method are more clear, as cmdlets with the New verb are usually used in variable assignments, i.e. for persistent objects.

Considerations

Start-Sleep's parameter structure must be updated. Ideally, a non-TimeSpan input should be interpreted as seconds, while explicitly providing the suffixes remains possible.
These suffixes ideally do not interfere with the existing suffixes, as the types of the numbers would already be determined by the time unit. For example, tick will always be Int64 (or in case of future changes the type used for TimeSpan's Ticks property)

Postscriptum

This issue pairs well with issue #10712.

Issue-Enhancement WG-Language

Most helpful comment

Also FWIW there is a string format you can currently pass to parameters typed as TimeSpan. A literal would definitely be more desirable, but since this isn't super commonly known here's some examples:

function Test-TimeSpan {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [timespan] $TimeSpan
    )
    end {
        return $TimeSpan
    }
}

filter Format-TimeSpan {
    $format = '{0:%d} Day(s), {0:%h} Hour(s), {0:%m} Minute(s), {0:%s} Second(s), {0:fff} Millisecond(s)'
    return ($format -f $PSItem) -replace '0+(?=\d+ Millisecond)', ''
}

Test-TimeSpan 10.23:59:59.999 | Format-TimeSpan
# 10 Day(s), 23 Hour(s), 59 Minute(s), 59 Second(s), 999 Millisecond(s)

Test-TimeSpan 10.0:30 | Format-TimeSpan
# 10 Day(s), 0 Hour(s), 30 Minute(s), 0 Second(s), 0 Millisecond(s)

Test-TimeSpan 0:30 | Format-TimeSpan
# 0 Day(s), 0 Hour(s), 30 Minute(s), 0 Second(s), 0 Millisecond(s)

Test-TimeSpan 0:0:.001 | Format-TimeSpan
# 0 Day(s), 0 Hour(s), 0 Minute(s), 0 Second(s), 1 Millisecond(s)

All 52 comments

I like the idea as a whole, it'd be nice to have a built in way to reference simple time spans. For the suffixes, I think a couple of them are a bit longer than necessary, though?

  • tk ticks
  • dy days

As you mention, years is problematic and I'd be tempted to simply not include a builtin for it. Even in Western calendars, the length of a year varies a decent amount at the best of times, not to mention leap years.

In terms of the impact of the change on existing scripts, though... any new number literals we add will restrict the pool of available command names slightly. If someone already has a command named (for example) 50ms, that command name will no longer resolve by default, and could only be called via the & operator.

I don't know if using this kind of command name is common or not, but it's also worth noting that users do have the ability to override CommandNotFoundAction and could be using that to imitate this kind of functionality.

On a more implementation focus, would you expect such suffixes to be able to be combined with some other suffix(es)? We have both type suffixes and multiplier suffixes already. I'd imagine multiplier suffixes make the most sense in terms of allowing them to be combined...

Not sure about the need for ticks but I like the idea in general. Here's an example in a language (C++) that has done something similar with std::literals::chrono_literals:

image

From https://en.cppreference.com/w/cpp/chrono/duration

An associated issue is that TimeSpan really needs more user-friendly output - perhaps humanized. Ideally, a single line of information:

1 second, 523 milliseconds
# or
1.523 seconds

as opposed to this:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 523
Ticks             : 15230000
TotalDays         : 1.76273148148148E-05
TotalHours        : 0.000423055555555556
TotalMinutes      : 0.0253833333333333
TotalSeconds      : 1.523
TotalMilliseconds : 1523

Adding to the point about readability of the output of TimeSpan - why does Ticks not follow the rest of the properties' patterns of having an X and a TotalX property? It would perhaps be more appropriate to have Ticks represent the part of the time span that's smaller than one millisecond, and TotalTicks represent what Ticks currently represents. Maybe that's reaching a bit, though.

That's a different issue entirely, and one that comes from .NET Core; PowerShell really has no say in the matter there (short of adding our own implementation of TimeSpan and deviating from the standard).

Best guess: Ticks is the underlying value in all cases, it's what TimeSpan actually _is_ on a fundamental level. The other properties are essentially just shortcuts so you don't have to bother figuring out the math yourself. Most folks using TimeSpan probably don't see a need for "ticks remaining after calculating the rest of the properties" sort of value.

Sounds plausible. When converting to a TimeSpan object, the value given in Integer form becomes the new tick count, after all.

On an unrelated note, since I'm new to open-source contributing in general, should I incorporate the suggestions posted here into the issue body, or is the intention that you read the whole thread to get the picture? Or should I wait until there's actual code to debate about?

@schuelermine Right workflow is to create new RFC for New-TimeSpan cmdlet in PowerShell-RFC repository. Please do not add other proposals in the RFC to get fast progress.

@iSazonov what does New-TimeSpan have to do with this, exactly?

As far as I can see, PowerShell-RFC doesn't require specifying a cmdlet for RFCs anyways.

I'm working on an RFC draft right now.

In terms of the impact of the change on existing scripts, though... any new number literals we add will restrict the pool of available command names slightly. If someone already has a command named (for example) 50ms, that command name will no longer resolve by default, and could only be called via the & operator.

Does it make sense to think about a potential "literal" operator? There's a ton of potential for new post fix operators that don't tend to go anywhere because of this.

No idea what symbol would make sense and not have the same problem though. For some reason I like the idea of ~10sec, but that can currently be a command as well and makes me read it as "about 10 seconds".

Also FWIW there is a string format you can currently pass to parameters typed as TimeSpan. A literal would definitely be more desirable, but since this isn't super commonly known here's some examples:

function Test-TimeSpan {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [timespan] $TimeSpan
    )
    end {
        return $TimeSpan
    }
}

filter Format-TimeSpan {
    $format = '{0:%d} Day(s), {0:%h} Hour(s), {0:%m} Minute(s), {0:%s} Second(s), {0:fff} Millisecond(s)'
    return ($format -f $PSItem) -replace '0+(?=\d+ Millisecond)', ''
}

Test-TimeSpan 10.23:59:59.999 | Format-TimeSpan
# 10 Day(s), 23 Hour(s), 59 Minute(s), 59 Second(s), 999 Millisecond(s)

Test-TimeSpan 10.0:30 | Format-TimeSpan
# 10 Day(s), 0 Hour(s), 30 Minute(s), 0 Second(s), 0 Millisecond(s)

Test-TimeSpan 0:30 | Format-TimeSpan
# 0 Day(s), 0 Hour(s), 30 Minute(s), 0 Second(s), 0 Millisecond(s)

Test-TimeSpan 0:0:.001 | Format-TimeSpan
# 0 Day(s), 0 Hour(s), 0 Minute(s), 0 Second(s), 1 Millisecond(s)

As far as I can see, PowerShell-RFC doesn't require specifying a cmdlet for RFCs anyways.

@schuelermine I ask you as maintainer and based on my experience. The new cmdlet looks very "powershelly" and can be approved very fast by PowerShell Committee. But other proposals are controversial and you will be doomed to wait a very long time (see other RFCs). So I suggest to create simple RFC for the cmdlet and move other proposal in another RFC.

If someone wants to tackle this hopeless topic (Timespan formatting), see # 2595 and related draft PR.

Does it make sense to think about a potential "literal" operator? There's a ton of potential for new post fix operators that don't tend to go anywhere because of this.

Ah, every time I think about this I come back to "prefix" pattern.
For me this looks very "powershelly":

[Second]12
[sec]15
[Day]3
[Minute]59
[min]59
[hour]23
[week]10
[century]20

Observations

  1. Currently the only Function or cmdlet with a seconds parameter appears to be start-sleep (based on get-command -Type Cmdlet,Function | where {$_.Parameters -and $_.parameters.ContainsKey("seconds")}
  2. Piping a timespan object into something with a seconds parameter with accept pipeline by property name will give the wrong result, timespan seconds is an int from 0 to 59,
  3. An argument transformer class could, quite easily, change any value passed to seconds as a timespan type to its TotalSeconds property.
  4. Such a class would be available to other cmdlets and functions which wanted to use it (but would likely need to be available to load independently so they would not be dependent on a new version of pwsh)

Does it make sense to think about a potential "literal" operator? There's a ton of potential for new post fix operators that don't tend to go anywhere because of this.

Personally I would say "no"
Today only one built in command takes a -Seconds parameter, and that is Start-Sleep. I suspect there are more in other modules, especially where one is adding a time out.
To date, people have managed to write 300 for 5 minutes, or 30 for 0.5 minutes, and it hasn't been a great problem (yes, I know, "we've always done it that way" is the worst reason ever)

Taking an existing function which has a -Timeout parameter and allowing the user to write
Invoke-MyOldFunction -Timeout 2m
Will simply break that function as it does not expect a TimeSpan object - this happens now when a timespan is passed to Start-Sleep.
For a small amount of effort saved writing
-Timeout ($m * 60) or -Timeout ($h * 3600)
more effort will be needed to remove [int] from timeout parameters and change
$endtime = $now.addSeconds($timeout)
statements to
If ($timeout -is [timespan]) {$endTime = $now.add($timeout)} else { .... etc

Even adding a transform class to turn timespans into total seconds would require re-coding. Since we can be fairly sure that
(a) Commands will need to keep accepting seconds as a number, to work with existing scripts and
(b) some commands won't be updated,
the smart thing for users to do is to continue to use seconds even if this is adopted.

If literals are to be used then PLEASE use the same naming as already applies to formatting strings, i.e. s or ss for second, m or mm for minute, h or hh for hours (should be no need for H/HH which is 24 hour clock while h/hh is 12 hour). d / dd for day. yy for year. (Year and Day are needed when specifying things like -MaximumAge, not so much for Sleep :-) ) Milliseconds and ticks can be expressed either as seconds or using f for fraction.

@iSazonov OK, I'm making a second RFC for just adding TimeSpan support to Start-Sleep

@jhoneill I don't quite follow the last paragraph. Could you list your proposed suffixes and their uses?

The second RFC will take a bit of time though, I have something else to do right now.

d / dd alone as a suffix is going to clash with hexadecimal literals and be unusable there. That aside, would be good to see a thorough list of your suggestions as @schuelermine says.

Today only one built in command takes a -Seconds parameter, and that is Start-Sleep. I suspect there are more in other modules, especially where one is adding a time out.

To date, people have managed to write 300 for 5 minutes, or 30 for 0.5 minutes, and it hasn't been a great problem (yes, I know, "we've always done it that way" is the worst reason ever)

Can you elaborate a little bit more in how that relates to timespan literals?

Taking an existing function which has a -Timeout parameter and allowing the user to write
Invoke-MyOldFunction -Timeout 2m
Will simply break that function as it does not expect a TimeSpan object - this happens now when a timespan is passed to Start-Sleep.

Nah, in the same way that Write-Host 1mb writes 1mb instead of 1048576. When in command argument parsing mode, a literal is created but wrapped in a PSObject with knowledge of the original token text.

For a small amount of effort saved writing
-Timeout ($m * 60) or -Timeout ($h * 3600)
more effort will be needed to remove [int] from timeout parameters and change
$endtime = $now.addSeconds($timeout)
statements to
If ($timeout -is [timespan]) {$endTime = $now.add($timeout)} else { .... etc

Even adding a transform class to turn timespans into total seconds would require re-coding. Since we can be fairly sure that
(a) Commands will need to keep accepting seconds as a number, to work with existing scripts and
(b) some commands won't be updated,
the smart thing for users to do is to continue to use seconds even if this is adopted.

If you don't type your parameter as TimeSpan, nothing will be different.

Ah, every time I think about this I come back to "prefix" pattern.
For me this looks very "powershelly":

[Second]12
[sec]15
[Day]3
[Minute]59
[min]59
[hour]23
[week]10
[century]20

Are those all types? Or would they really convert to TimeSpan? We sort of have existing syntax like that with PSCustomObject really pointing to PSObject and it confuses the heck out of folks. Also you can't really do mixed measurements like "1 day 10 hours". Also you'd need to wrap it in a paren expression before you could use it in a command argument.

Also side note, a lot of the discussion seems to be around command arguments but the uses for this are a lot more imo. For instance, I'd love to be able to do something like this:

$threshold = (Get-Date) - 30days
Get-ChildItem | ? LastWriteTime -gt $threshold

@jhoneill I don't quite follow the last paragraph. Could you list your proposed suffixes and their uses?

OK. To convert date time to string we use "yyyy MM dd HH:mm:ss:fffff"
for year, Month, day, Hour (24 hour clock in this case), minutes, seconds and fractions of seconds

if we then say in one place we use h or "hh" to mean hours and somewhere else we write "hour" it will lead to error.
So I would support the same values as used in .NET format strings. However as already pointed out d and f would clash with supporting hex numbers.

Ah, every time I think about this I come back to "prefix" pattern.
For me this looks very "powershelly":

[Second]12
[sec]15
[Day]3
[Minute]59
[min]59
[hour]23
[week]10
[century]20

Are those all types? Or would they really convert to TimeSpan? We sort of have existing syntax like that with PSCustomObject really pointing to PSObject and it confuses the heck out of folks. Also you can't really do mixed measurements like "1 day 10 hours". Also you'd need to wrap it in a paren expression before you could use it in a command argument.

Having those as types which derive from Timespan so [day]30 created a timespan of 30 days would
would give you

$threshold = (Get-Date) - [day]30
Get-ChildItem | ? LastWriteTime -gt $threshold

You would lack the ability to use them directly as arguments, though.

Get-ChildItem | Where-Object LastWriteTime -gt 10days
# vs
Get-ChildItem | Where-Object LastWriteTime -gt ([days]10)

That option is (IMO) far more awkward. Also, it means having multiple aliases to the same type which do very different things... and as @SeeminglyScience the only case where we currently do this is [PSCustomObject] which is already a pretty sore point of confusion for many folks.

Also you can't inherit TimeSpan as it's a struct. Each one would have to either be their own type completely or just be aliases to TimeSpan with some parser magic (like @vexx32 described currently occurs with PSCustomObject).

You would lack the ability to use them directly as arguments, though.

Get-ChildItem | Where-Object LastWriteTime -gt 10days
# vs
Get-ChildItem | Where-Object LastWriteTime -gt ([days]10)

That's comparing a datetime with a timespan, so the parameter value will always be an expression. Today I use [datetime]::now.addDays(-10) which is clunky.
but I don't think [datetime]::now.subtract(10days) is a great deal better.

Where it something is naturally a time span I agree ... 10days is more natural to write.

Also you can't inherit TimeSpan as it's a struct. Each one would have to either be their own type completely or just be aliases to TimeSpan with some parser magic

Darn. Parser magic is best avoided.

That's comparing a datetime with a timespan, so the parameter value will always be an expression.

I mean sure but you get what he means. Here's a more "correct" example:

Set-Date -Adjust 10days
# vs
Set-Date -Adjust ([days]10)
````

> Today I use `[datetime]::now.addDays(-10)` which is clunky.
> but I don't think `[datetime]::now.subtract(10days)` is a great deal better.

That isn't what is being proposed, it would be more like this:

```powershell
[datetime]::Now - 10days

Yes, I did get what he meant, but I couldn't think of a built in command which takes a timespan parameter.
>(get-command -type Cmdlet,Function | where {$_.parameters.values.parametertype.name -contains "timespan"})

Says the only one is Set-date :-)

I don't think anyone has done a survey to see where commands ask for age, timeout or whatever as a timespan and where they assume units. Built in we have Start-Sleep which can't take a time span, and Set-Date -adjust which must take one. which isn't much of a sample.

But the complaint that you can't write $now - $then -gt [days]10 and it needs to be $now - $then -gt ([days]10) also applies to -gt (date - 10days)

Changing start-sleep so it could take a timespan and introducing these literals mean you go from typing sleep -s[tab] 10 and reading "Sleep -seconds 10"
to Sleep -t[tab] 10secs and reading "Sleep -timespan 10secs" , but if this goes in a new version the writer is better sticking to -seconds 10 for compatibility. And if I have a timeout in my module, I have to add support for timespan, and if I haven't, or users are working with an old version they need to stick with seconds. It seems to me like low gain for fairly high cost.

I don't think anyone has done a survey to see where commands ask for age, timeout or whatever as a timespan and where they assume units. Built in we have Start-Sleep which can't take a time span, and Set-Date -adjust which must take one. which isn't much of a sample.

To be fair 100% of people don't use teleporters, doesn't mean we shouldn't invent one. Using TimeSpan kinda sucks right now, so no one uses it.

But the complaint that you can't write $now - $then -gt [days]10 and it needs to be $now - $then -gt ([days]10) also applies to -gt (date - 10days)

Those are two very different expressions you're comparing. Yeah if you need a binary expression as a command argument you'll need to wrap it in a paren expression, but that doesn't stop -gt 10days from being valid.

Changing start-sleep so it could take a timespan and introducing these literals mean you go from typing sleep -s[tab] 10 and reading "Sleep -seconds 10"
to Sleep -t[tab] 10secs and reading "Sleep -timespan 10secs"

More accurately Sleep 10s. The TimeSpan positional parameter would be a different parameter set, and the selected set would be based on typing.

but if this goes in a new version the writer is better sticking to -seconds 10 for compatibility.

It's certainly up to the individual whether they'd like to adopt new features of the language, but this would be far from the first one added. Would it be likely that someone would require whatever version this ships in to run their module specifically for this feature? Nah probably not, but if they already require it then there's no issue using it.

Plus, that doesn't apply to interactive use.

And if I have a timeout in my module, I have to add support for timespan, and if I haven't, or users are working with an old version they need to stick with seconds. It seems to me like low gain for fairly high cost.

Adding a parameter set isn't that big of a lift imo.

I don't think anyone has done a survey to see where commands ask for age, timeout or whatever as a timespan and where they assume units. Built in we have Start-Sleep which can't take a time span, and Set-Date -adjust which must take one. which isn't much of a sample.

To be fair 100% of people don't use teleporters, doesn't mean we shouldn't invent one. Using TimeSpan kinda sucks right now, so no one uses it.

That wasn't my point. There is only 1 command with a seconds parameter and one which takes a timespan from my installation of PowerShell. There are bound to be others in modules which I don't have, but the number is hard to quantify. But if you want to be on the beta program for teleport .... :-) DateTime and TimeSpan are what come from .NET and there is a valid discussion to be had around what should PowerShell wrap around them to make either/both less sucky.

... doesn't stop -gt 10days from being valid.
No. But at present (and maybe because timespan is a bit sucky) the number of places you would compare two timespans is pretty small. $x -gt 10days is fine a nice bit of polish if everything else has been, but $x.totaldays -gt 10 is not terrible.

if this goes in a new version the writer is better sticking to -seconds 10 for compatibility.

It's certainly up to the individual whether they'd like to adopt new features of the language,

It isn't though. ... all first-party cmdlets which specify any kind of duration would need to add TimeSpan support ; e.g. we have Invoke-RestMethod -TimeoutSec so first we need to figure out how that will support timespans. Since -Ti is unique now, any new parameter must use different naming to avoid a breaking change, and the least work is the _transformer attribute_ route so people can write -TimeOut 10m and a 10 minute timespan morphs into int 600.
BUT 3rd party modules won't catch up immediately and some won't see the point. So the user quickly learns than 600 works as seconds pretty much everywhere, because it always has, but 10m crashes in most places - which teaches the behaviour of not using it.

... add support for timespan, and if I haven't, or users are working with an old version they need to stick with seconds. It seems to me like low gain for fairly high cost.

Adding a parameter set isn't that big of a lift imo.

No, it's not big, and adding the conditional logic for each is also a small overhead. If you have 200 functions which take a time out - e.g. a large module which calls rest APIs, Its 200x 2 small tasks - and I have two modules which call invoke-restMethod from ~200 places each, if it is changed to support timespans would I bother updating so my functions did. Maybe, when I had run out of other things to do; but in the short term I'd be one of those things which didn't work and got in the way of adoption.

As I said before I'd rather have an argument converter class so if someone passed a timespan to something expecting a number of seconds/milliseconds/nano-fortnights/whatever it just worked. A quick way to make timespans, well why not , if someone can make the literals workable, and international that's great.
But there's no great pressure for timespans as parameters because people don't feel an overpowering need to use one, and no one has cried out for easy ways to make a timespan because (a) [timespan]100 or [timespan]::FromHours(1.5) aren't so terribly, impossibly hard, and (b) the number of places you use them is quite small so although that syntax is clunky it's not annoying day in, day out.
It's just seems to me this would be too far down the list of nice-to-haves to ever get worked on.

Also you can't really do mixed measurements like "1 day 10 hours".

This is a right thing ("1 day 10 hours") for PowerShell - it follow English notation like cmdlet names.

And we can easily implement [TimeSpan]"1 day 10 hours" but it is difficult to get this for arguments.

No. But at present (and maybe because timespan is a bit sucky) the number of places you would compare two timespans is pretty small. $x -gt 10days is fine a nice bit of polish if everything else has been, but $x.totaldays -gt 10 is not terrible.

I think you're reading into the example of Where-Object too specifically. The example wasn't about comparisons, it's about command argument parsing.

It isn't though. ... all first-party cmdlets which specify any kind of duration would need to add TimeSpan support

Ideally they would add that, but they defiinitely wouldn't have to and likely wouldn't. If anything you'd probably see more argument transformation attributes that allow those existing parameters to simply refer to TotalSeconds when passed a TimeSpan (as you later mention).

BUT 3rd party modules won't catch up immediately and some won't see the point. So the user quickly learns than 600 works as seconds pretty much everywhere, because it always has, but 10m crashes in most places - which teaches the behaviour of not using it.

I don't really think this is an issue. Especially if the argument transformation attributes were public and surfaced through Get-Help.

... add support for timespan, and if I haven't, or users are working with an old version they need to stick with seconds. It seems to me like low gain for fairly high cost.

I strongly disagree that the cost is high for adopters or consumers.

Maybe, when I had run out of other things to do; but in the short term I'd be one of those things which didn't work and got in the way of adoption.

Yeah and that's fine.

But there's no great pressure for timespans as parameters because people don't feel an overpowering need to use one, and no one has cried out for easy ways to make a timespan because (a) [timespan]100 or [timespan]::FromHours(1.5) aren't so terribly, impossibly hard, and (b) the number of places you use them is quite small so although that syntax is clunky it's not annoying day in, day out.

Yes, people don't feel an overpowering need to use one because nothing in PowerShell currently takes them. Nothing in PowerShell currently takes them because the syntax is bad.

No. But at present (and maybe because timespan is a bit sucky) the number of places you would compare two timespans is pretty small. $x -gt 10days is fine a nice bit of polish if everything else has been, but $x.totaldays -gt 10 is not terrible.

I think you're reading into the example of Where-Object too specifically. The example wasn't about comparisons, it's about command argument parsing.
There are really only two uses for a time span an input. Is the difference between these two times less than / greater than X. And Add/Subtract this difference to a time. So it's either an operand in an comparison or its a parameter. However the reality is today we can only name one command which takes time span as an input, and everything else takes duration in seconds, or some multiple of seconds.

It isn't though. ... all first-party cmdlets which specify any kind of duration would need to add TimeSpan support

Ideally they would add that, but they defiinitely wouldn't _have_ to and likely wouldn't. If anything you'd probably see more argument transformation attributes that allow those existing parameters to simply refer to TotalSeconds when passed a TimeSpan (as you later mention).

I think (and other views are valid) that if you introduce something which says "hey, are you sick of multiplying by 60, just write 5m instead" it needs to work in as many places as possible, otherwise the user tries it with cmdlet a, fails, cmdlet b, fails, cmdlet c, fails, and goes back to using seconds "because the new way never works". And you end up with a bit of arcane syntax which nobody actually uses, so no one supports, and we're back we started.
How you support it is another matter and the transformer makes coding and use simple.

BUT 3rd party modules won't catch up immediately and some won't see the point. So the user quickly learns than 600 works as seconds pretty much everywhere, because it always has, but 10m crashes in most places - which teaches the behaviour of not using it.

I don't really think this is an issue. Especially if the argument transformation attributes were public and surfaced through Get-Help.
Not sure that attributes are, currently. But if the user finds that, OK start-sleep will use it, but this such-and-such a module which calls rest-apis needs seconds, and something else which does SQL queries needs seconds, and that one which tests connectivity needs seconds, then they don't bother using the new way.

... add support for timespan, and if I haven't, or users are working with an old version they need to stick with seconds. It seems to me like low gain for fairly high cost.

I strongly disagree that the cost is high for adopters or consumers.

No, the cost for people using it is almost nil. They just have to see which commands support the new way for as long as the persevere with it. For me as a developer with 400 commands in two modules which might take a timeout to pass through to Invoke-webRequest I've got a job on my hands to add and test support for timespans as an alternative to seconds. But if users try using it in a few places and give up I'm doing something no-one will use. So effort has gone in, but no-one thinks the product is improved.

Yes, people don't feel an overpowering need to use one because nothing in PowerShell currently takes them. Nothing in PowerShell currently takes them because the syntax is bad.

And because it isn't so hard to work in SI units of time (seconds). If you don't make it easier to do than seconds people will fall back to that.

FYI, I can't write a seperate RFC for just Start-Sleep right now, I have some other work to do.

Interesting, can we consider days or secs (or -days) as unary operator in 10 days expression?

馃 interesting question.

_maybe_?

-days would be more consistent with the pattern of operators, but we don't (currently) have any unary postfix operators; all our unary operators are prefixes ( -join $array for example)

31 -days -eq ($date - $pastDate) looks a little funny.

I'm thinking that starts to blur the line between what is an operator and what is (effectively) a value. the -days is part of the intended value itself, really. I think I still prefer the suffix on the numeral itself.

I am thinking about something universal, extendable. Suffixes looks more problematic in the case.

Another thought - we could consider common timespan formats https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings This can slow down parser.

I don't see this as too different from the currently supported suffixes e.g.:

04-16 18:18:22 1> 10mb
10485760

One difference is that the current suffixes combine only via the various math operators e.g. +/-. I could see using something like -1day or 10min a lot. And combining with + isn't too bad 1day + 2hr + 5min + 2sec. Also, like the kb/mb/gb/tb suffixes you could use a float value e.g 1.0868day, 1.5hr, etc.

Thinking about it, maybe splitting this up into two RFCs is a good idea, one for Start-Sleep accepting TimeSpan and one for TimeSpan literals. Then again, the first is much more useful with the second (and vice-versa).

Is an RFC really necessary for the Start-Sleep aspect of this request?

This should do IMO https://github.com/IISResetMe/PowerShell/commit/1e9cff9c8cb5f45929056b45f68eede214e3b9db

image

I realize the screencap does a poor job of conveying the fourth dimension, but you get the idea 馃槃

Can we add a type converter from string to TimeSpan that handles the suffices?

These suffices is on English that is not friendly for other languages.

Localization is indeed something to think about... But I think ultimately if we match the shorthands used in standard format strings or variants thereof we should be fine.

These suffices is on English that is not friendly for other languages.

Well, we are stuck with English, aren't we?

All the operators, keywords, types and commands are in English, so why shouldn't these suffices be the same?

I had some holiday morning fun and wrote a simple type converter from string to TimeSpan.

[timespan] "1d -5m" | % tostring
23:55:00

[timespan] "1d 2h 33s 2ms" | % tostring
1.02:00:33.0020000

This could quite easily be used with a datetime converter to:
think

[datetime] 'a year ago'
tisdag 21 maj 2019 11:47:03
[datetime] 'in 2 months'
tisdag 21 juli 2020 11:47:27
public sealed class StringToTimespanTypeConverter : PSTypeConverter
{
    /// <inheritdoc />
    public override bool CanConvertFrom(object sourceValue, Type destinationType)
    {
        return destinationType == typeof(TimeSpan) && sourceValue is string;
    }

    enum TimeSuffix
    {
        None,
        Millisecond,
        Second,
        Minute,
        Hour,
        Day,
        Year
    }

    /// <inheritdoc />
    public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
    {
        static (int num, TimeSuffix suffix) GetNext(ref ReadOnlySpan<char> span)
        {
            if (span.Length == 0)
                return (0, TimeSuffix.None);
            var endIndex = span.IndexOf(' ');
            if (endIndex == -1) endIndex = span.Length;

            var current = span[0..endIndex];

            while (endIndex < span.Length && span[endIndex].IsWhitespace())
            {
                endIndex++;
            }

            span = span[endIndex..];

            int i = 0;
            while (i < current.Length && (current[i].IsDecimalDigit() || current[i].IsDash()))
            {
                i++;
            }

            if (i == 0)
            {
                return (0, TimeSuffix.None);
            }

            var number = current[0..i];

            if (!int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out int num))
            {
                return (0, TimeSuffix.None);
            }
            var suffix = current[i..];

            return suffix.Length switch
            {
                1 => suffix[0] switch
                {
                    'd' => (num, TimeSuffix.Day),
                    'h' => (num, TimeSuffix.Hour),
                    'm' => (num, TimeSuffix.Minute),
                    's' => (num, TimeSuffix.Second),
                    'y' => (num, TimeSuffix.Year),
                    _ => (0, TimeSuffix.None),
                },
                2 => "ms".AsSpan().SequenceEqual(suffix)
                    ? (num, TimeSuffix.Millisecond)
                    : (0, TimeSuffix.None),
                _ => (0, TimeSuffix.None),
            };
        }

        if (sourceValue is string str)
        {
            var time = new TimeSpan();
            var span = str.AsSpan();
            while (span.Length != 0)
            {
                (int num, TimeSuffix suffix) =  GetNext(ref span);
                if (suffix == TimeSuffix.None) return TimeSpan.Parse(str);
                var delta = suffix switch
                {                
                    TimeSuffix.Millisecond => TimeSpan.FromMilliseconds(num),
                    TimeSuffix.Second => TimeSpan.FromSeconds(num),
                    TimeSuffix.Minute => TimeSpan.FromMinutes(num),
                    TimeSuffix.Hour => TimeSpan.FromHours(num),
                    TimeSuffix.Day => TimeSpan.FromDays(num),
                    TimeSuffix.Year=> TimeSpan.FromDays(num*365),
                    _ => throw new ArgumentOutOfRangeException()
                };

                time += delta;
            }

            return time;
        }

        throw new PSInvalidCastException("InvalidCastNotAValidTimespan", $"Cannot convert {sourceValue} to {destinationType}", null);
    }

    /// <inheritdoc />
    public override bool CanConvertTo(object sourceValue, Type destinationType)
    {
        return false;
    }

    /// <inheritdoc />
    public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase)
    {
        throw new NotImplementedException();
    }
}

Just to clarify: This enables us to use these string in all places where a command uses a TimeSpan parameter.

Just wanted to say sorry for not being involved in this recently. My stuff has been a mess recently, sorry

Hey there! I found this discussion because I was about to propose the same API as @IISResetMe did in IISResetMe@1e9cff9. Regardless of how the TimeSpan object gets created, we'll still need to make changes like this one to allow the cmdlets to accept them, correct?

If so, is there anything blocking us from moving forward with that part of the change now?

Don't think so... it's a sensible change to make, really, regardless of whether we get a base timespan syntax built in.

@MattKotsenas nothing's blocking, I just haven't found the time to move it forward :)

There's one question I've been pondering that I'd like to be able to answer before submitting the PR:

  • Should we treat negative [timespan] values as _durations_, _zero-valued_ or _invalid_?

In other words, should this statement sleep for 5 seconds, 0 seconds or throw a terminating error:

$negative5Seconds = -(New-TimeSpan -Seconds 5)

Start-Sleep $negative5Seconds

Implementation in https://github.com/IISResetMe/PowerShell/commit/1e9cff9 treats it as a duration and sleeps for 5 seconds, but I haven't fully convinced myself that it should... 馃

I would expect that Start-Sleep would behave similarly between the different parameters, and well as analogous APIs like Thread.Sleep().

Start-Sleep -Seconds -5

System.Management.Automation.ParameterBindingValidationException: Cannot validate argument on parameter 'Seconds'. The -5 argument is less than the minimum allowed range of 0. Supply an argument that is greater than or equal to 0 and then try the command again.

[System.Threading.Thread]::Sleep([TimeSpan]::FromSeconds(-5))

ArgumentOutOfRangeException: Number must be either non-negative and less than or equal to Int32.MaxValue or -1 (Parameter 'timeout')

So my vote would be for an error.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

concentrateddon picture concentrateddon  路  3Comments

MaximoTrinidad picture MaximoTrinidad  路  3Comments

manofspirit picture manofspirit  路  3Comments

JohnLBevan picture JohnLBevan  路  3Comments

SteveL-MSFT picture SteveL-MSFT  路  3Comments