I have created a Powershell/XAML app, that on button press makes a RESTAPI call, parses the JSON response into fields in the app front end. All fine so far.
These fields will be populated with a string representing a time, so "1800" or "2000" etc.
The user can then change this from 1800 to 1900 for example.
This is all fine, and in the background the app will use 1900 to update a setting to be used in a POST back.
However there are other settings that are offset by 90 mins of the time above. I don't want the user to have update each one, which is why I am trying to programmatically.
But try as I might, I cannot take a string of 1800, add 90 mins to it and make the value 1930 (not 1890).
You could parse the input as a DateTime object (ignoring the date part) and then use the AddMinutes method.
$input = '1800'
$hour = $input.Substring(0,2)
$minute = $input.Substring(2,2)
$dateInputStr = "0001-01-01,${hour}:${minute}:00"
[datetime]$dateInput = ([datetime]$dateInputStr)
$dateInput = $dateInput.AddMinutes(90)
$dateInput.ToString("HHmm")
Using [timespan] instances is another option:
$time = '1800'
([timespan] ($time -replace '(?<=^..)', ':') + '01:30').ToString('hhmm') #->'1930'
$time -replace '(?<=^..)', ':' uses the regex-based -replace operator to insert : after the first two characters - see this regex101.com page for an explanation of the regex and the ability to experiment with it.
Due to expressing the results only in terms of hours and minutes, the calculation wraps around at midnight, so that adding '05:30', for instance, would yield '0030'
The RHS operand needn't be cast to [timespan] directly, because the data type of the LHS - with its explicit [timespan] cast - implicitly converts the RHS to [timespan] too, with '01:30' representing 1 hour and 30 minutes, i.e. 90 minutes.
If you want to define the duration to add in terms of 90 minutes, use the following instead (there are analogous static methods for other units, such as ::FromSeconds():
[timespan]::FromMinutes(90)
Alternatively, you can cast a number to [timespan], which is interpreted as ticks, which are 100-nanosecond units; there are 1e9 (10 to the power of 9) nanoseconds in a second, and therefore 1e7 100-nanosecond units in a second. Thus, multiplying with 1e7 gives you seconds, and multiplying that with 60 minutes.
# 90 minutes expressed as ticks
[timespan] 90 * (60 * 1e7)
When I read this question I wanted to solve it with minimal string manipulation, leaning on time related objects and methods instead. datetime was the first object I thought of, but it expects a date (year, month, day). Things actually simplify if we use timespan. Its static method, ParseExact, can parse the string directly.
$offsetTimeSpan = [timespan]::FromMinutes(90)
$timeField = '830'
$timeStr = $timeField.PadLeft(4, '0')
$timeSpan = [timespan]::ParseExact($timeStr, 'hhmm', [CultureInfo]::InvariantCulture)
$offsetTime = $timeSpan.Add($offsetTimeSpan)
$offsetTime.ToString('hhmm')
$timeField is used to represent the time you get from the RESTAPI. PadLeft is only needed if it's possible for a leading 0 to be missing. ParseExact does the heavy lifting of converting the string to a time type. Because timespan doesn't have an AddMinutes member, we use the Add method passing in a timespan of 90 minutes, $offsetTimeSpan.
You don't mention anything about overflowing past midnight. You can test for overflow using $offsetTime.Days, if any special processing is required.
Related
I'm creating an application in PowerShell to reschedule the existing jobs in a SQL Server instance. So I have to get the active_start_time value from the sysschules table. The time value is formatted as INT HHMMSS on a 24-hour clock.
As I am using the JobSchedule class (Microsoft.SqlServer.Management.Smo.Agent.JobSchedule) in my PowerShell application, I need to convert from time value in INT to a TimeSpan value in order add to ActiveStartTimeOfDay property.
Example:
Original data: [INT] active_start_time = 10500
Expected data: [TimeSpan] ActiveStartTimeOfDay = 01h 05 min 00s
As FoxDeploy points out, treat the input number as a string!
First, you'll want to use String.PadLeft() to zero-pad the number:
$active_start_time = 10500
$start_timestamp = "$active_start_time".PadLeft(6, '0') # "010500"
Now that we have a "timestamp" of sorts, [timespan] has a ParseExact() method we can use to parse any format:
$start_timespan = [timespan]::ParseExact($start_timestamp, 'hhmmss', $null)
We can do this by taking your input int and treating it like a string. Doing that lets us call ToCharArray() which does this.
$myString = "Hi!"
$myString.ToCharArray()
H
i
!
Once it's in a char array, we can select out the positions of the characters we want by specifying its index, starting with zero, like this:
PS> $myString.ToCharArray()[0] #get's the first
H
Applying that to your scenario, we do the same to select only the numbers we want and tuck them into variables to call on later. With that, we can easily build a new [TimeSpan] object.
$inputInt = 121515
$hours= $inputInt.ToString().ToCharArray()[0..1] -join ''
$mins= $inputInt.ToString().ToCharArray()[2..3] -join ''
$secs = $inputInt.ToString().ToCharArray()[4..5] -join ''
$hours,$mins,$secs -join ":"
Output> 12:15:15
There are a lot of options to make a new [TimeSpan], we can see our choices by typing the static class name and method, and seeing what comes up.
PS> [timespan]::new
OverloadDefinitions
-------------------
timespan new(long ticks)
timespan new(int hours, int minutes, int seconds)
timespan new(int days, int hours, int minutes, int seconds)
timespan new(int days, int hours, int minutes, int seconds, int milliseconds)
That second one looks promising...
$newTimeSpan = [TimeSpan]::new($hours,$mins,$secs)
$newTimeSpan
This should get you heading in the right direction.
In Julia, you may generate a date range by month like this:
julia> dr = Date(2014,1,29):Dates.Month(1):Date(2014,07,29)
Date("2014-01-29"):Month(1):Date("2014-07-29")
julia> collect(dr)
7-element Array{Date,1}:
2014-01-29
2014-02-28
2014-03-29
2014-04-29
2014-05-29
2014-06-29
2014-07-29
Date(2014,1,29) is the start date, Dates.Month(1) is the step, Date(2014,07,29) is the end date.
Raku has a method later, but when used in custom generator, it lead to infinite range:
lazy my #dates = Date.new('2014-01-29'), Date.new('2014-02-28'), { $^a.later(:1month) } ... Date.new('2014-07-29')
If I use * >= Date.new('2014-07-29') instead of Date.new('2014-07-29') on the right of ... operator, it works:
lazy my #a = Date.new('2014-01-29'), Date.new('2014-02-28'), { $^a.later(:1month) } ... * >= Date.new('2014-07-29')
2014-01-29
2014-02-28
2014-03-28
2014-04-28
2014-05-28
2014-06-28
2014-07-28
2014-08-28
why the custom generator { $^a.later(:1month) } in the { $^a.later(:1month) } ... Date.new('2014-07-29') doesn't stop at 2014-07-29 and lead to infinite range?
As JJMerelo indicates in the comments, the way that the ... operator works is to continue generating elements based on the left-hand-arguments until the right-hand-argument is (per smartmatching) exactly reached.
For example, if we made a sequence of multiples of 10s,
my #tens = 0, 10, 20 ... 95;
say #tens[10]; # 100
say #tens[11]; # 110
This is because not element of #tens will actually be 95. To determine if an element is the final one, the smartmatch operator (~~) is used. Smartmatching a DateTime with another Datetime returns true if the two represent the same time (which may be different nominal times because of timezones, etc).
For sequences, DateTime is further complicated by the fact that .later and .earlier are not communative, so doing $date.later(:1month).later(:1month) is not guaranteed to give the same result as $date.later(:2month).
The reason that * ≥ DateTime.new(…) is different is that smartmatching for Callable objects (which that technically is, it's whatever code equivalent to anon sub $dt { $dt ≥ DateTime.new(…) } passes the left hand argument to the callable. If you're not 100% sure that a sequence will terminate by reaching an exact value, it's best to use the whatever code approach to ensure a value eventually matches.
Hmmm - for any kids out there trying this, there are some pitfalls to do with "what I mean"... for example when I try this
lazy my #b = Date.new('2014-01-31'), Date.new('2014-02-28'), { $^a.later(:1month) } ... * >= Date.new('2014-07-29')
I get this...
#(2014-01-31 2014-02-28 2014-03-28 ...)
But maybe I wanted the last day of each month, what if 2014 is a leap year, blah, blah
So if you want the last day (or last day -1), then there is also this handy method...
say Date.new('2015-11-24').last-date-in-month;
I'm adding a second to an instance of Foundation's date, but the result is off by an entire minute.
var calendar = Calendar(identifier: .iso8601)
calendar.locale = Locale(identifier: "en")
calendar.timeZone = TimeZone(identifier: "GMT")!
let date1 = Date(timeIntervalSinceReferenceDate: -62544967141.9)
let date2 = calendar.date(byAdding: DateComponents(second: 1),
to: date1,
wrappingComponents: true)!
ISO8601DateFormatter().string(from: date1) // => 0019-01-11T22:00:58Z
ISO8601DateFormatter().string(from: date2) // => 0019-01-11T21:59:59Z
Interestingly, one of the following makes the error go away:
round time interval since reference date
don't add time zone to calendar
set wrappingComponents to false (even though it shouldn't wrap in this case)
I don't really need sub-second precision in my code, so I created this extension that allows me to discard it.
extension Date {
func roundedToSeconds() -> Date {
return Date(timeIntervalSinceReferenceDate: round(timeIntervalSinceReferenceDate))
}
}
I want to know this:
Why does this error happen?
Am I doing something wrong?
Is there any issue with my workaround?
Why does this error happen?
I would say this is a bug in Core Foundation (CF).
Calendar.date(byAdding:to:wrappingComponents:) calls down to the internal Core Foundation function _CFCalendarAddComponentsV, which in turn uses the ICU Calendar C API. ICU represents a time as an floating-point number of milliseconds since the Unix epoch, while CF uses a floating-point number of seconds since the NeXT reference date. So CF has to convert its representation to ICU's representation before calling into ICU, and convert back to return the result to you.
Here's how it converts from a CF timestamp to an ICU timestamp:
double startingInt;
double startingFrac = modf(*atp, &startingInt);
UDate udate = (startingInt + kCFAbsoluteTimeIntervalSince1970) * 1000.0;
The modf function splits a floating-point number into its integer and fractional parts. Let's plug in your example date:
var startingInt: Double = 0
var startingFrac: Double = modf(date1.timeIntervalSinceReferenceDate, &startingInt)
print(startingInt, startingFrac)
// Output:
-62544967141.0 -0.9000015258789062
Next, CF calls __CFCalendarAdd to add one second to -62544967141. Note that -62544967141 lies in the round one-minute interval -62544967200 ..< -62544967140.0. So when CF adds one second to -62544967141, it gets -62544967140, which would be in the next round one-minute interval. Since you specified wrapping components, CF isn't allowed to change the minute part of the date, so it wraps back to the beginning of the original round one-minute interval, -62544967200.
Finally, CF converts the ICU time back to a CF time, adding in the fractional part of the original time:
*atp = (udate / 1000.0) - kCFAbsoluteTimeIntervalSince1970 + startingFrac + (nanosecond * 1.0e-9);
So it returns -62544967200 + -0.9000015258789062 = -62544967200.9, exactly 59 seconds earlier than the input time.
Am I doing something wrong?
No, the bug is in CF, not in your code.
Is there any issue with my workaround?
If you don't need sub-second precision, your workaround should be fine.
I can reproduce it with more recent dates but so far only with negative reference dates, e.g. Date(timeIntervalSinceReferenceDate: -1008899941.9), which is 1969-01-11T22:00:58Z.
Any negative timeIntervalSinceReferenceDate in the last second of a minute interval should cause the problem. The bug effectively makes the first round whole minute prior to time 0 span from -60.99999999999999 through -1.0, but it should span from -60.0 through -5e324. All more-negative round minute intervals are similarly offset.
Is there a method in matlab to convert seconds from a known date to a standard date time format?
For example, if I have a vector of values shown as seconds from 1901/01/01, how would I convert them to a dateTime? In this case a value of 28125 would correspond to 1981/01/01. Is there an efficient method for doing this?
The numbers in your example do not make sense so it is not clear if your time is in seconds or days but since you asked for seconds I will use this.
What you want to achieve can be done using datenum function. This function returns the number of (fractional) days from 1/1/0000. So first you need to find your offset, e.g.:
offsetInDays = datenum(1901,1,1);
Next, you convert the date from seconds to days:
dateInDays = YourRequiredDateInSec * 3600 * 24;
Finally, you date is given by
RequiredDate = datestr(offsetInDays + dateInDays);
I have two timestamps created with time:now() (one stored in an app variable from the past, one the current time). I need to find the difference between them (preferably in minutes). How do I do that?
I've tried this syntax, but the parser didn't like it:
diff = time:now() - original_time;
time:compare() doesn't give me enough information, and time:add() is the opposite of what I need. There don't seem to be any other applicable time functions documented.
The time functions return a time string, not a time object. To calculate time elapsed, you will have to convert your time string into epoch time (seconds since 1970..). Fortunately, epoch time is one of the formats supported by strftime.
foo = time:now();
efoo = time:strftime(foo,"%s);
The minus operator is actually sensitive to a leading whitespace. It's on the list of things to work out of the parser, but I just haven't had time to get to it. Here is a working rule:
rule first_rule {
select when pageview ".*" setting ()
pre {
foo = time:now();
bar = time:add(foo,{"minutes": -5});
ebar = time:strftime(bar,"%s");
efoo = time:strftime(foo,"%s");
diff = efoo-ebar;
}
notify("-5 minutes in seconds", diff) with sticky = true;
}