Date conversion going wrong with different computer language - powershell

I wrote a Powershell script for billing of customers. I get the list of bills, detect the month and get that specific bill. The timestamps are in Unix format and somehow I mess something up in the conversion depending on system language.
For example:
$FirstDayPrevMonth = (get-date -Day 1 -Hour 0 -Minute 0 -second 0 -Millisecond 0).Addmonths(-1)
The conversion fails for systems that return: zondag 1 november 2020 00:00:00. The conversion succeeds for systems that return: Sunday, November 1, 2020 12:00:00 AM
The timestamp I get from the bills list is for example: 1604188800000
The I run the following function passing 1604188800000, which should return number of the month:
Function Convert-FromUnixDateToMonth ($UnixDate) {
(get-date( [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').Addmilliseconds($UnixDate)).ToString("MM/dd/yyyy") ) ) -as [int] ### -UFormat %m
}
The result for the english system is 11 (correct), for the Dutch system is 1 (incorrect). I know it is somewhere in the language setting but I just can't figure out how to change this so it would work independent of the language.

Try below function to convert a Unix timestamp to either Local or UTC DateTime object
function ConvertFrom-UnixTimeStamp([Int64]$UnixTimeStamp, [switch]$AsUTC) {
while ($UnixTimeStamp -lt -62135596800 -or $UnixTimeStamp -gt 253402300799) {
# Assume $UnixTimeStamp to include milli- or nano seconds
$UnixTimeStamp = [int64][math]::Truncate([double]$UnixTimeStamp / 1000)
}
if ($UnixTimeStamp -gt [Int32]::MaxValue) {
# see: https://en.wikipedia.org/wiki/Year_2038_problem
Write-Warning "The given value exceeds the [Int32]::MaxValue of 2147483647 and therefore enters the Year2038 Unix bug.."
}
# the Unix Epoch is January 1, 1970 midnight in UTC
# older PowerShell versions use:
# [DateTime]$epoch = New-Object System.DateTime 1970, 1, 1, 0, 0, 0, 0, Utc
[DateTime]$epoch = [DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 'Utc')
$date = $epoch.AddSeconds($UnixTimeStamp)
if ($AsUTC) { $date } else { $date.ToLocalTime() }
# or use:
# if ($AsUTC) { [DateTimeOffset]::FromUnixTimeSeconds($UnixTimeStamp).UtcDateTime }
# else { [DateTimeOffset]::FromUnixTimeSeconds($UnixTimeStamp).LocalDateTime }
}
For your purposes, get the Month by using it like
(ConvertFrom-UnixTimeStamp 1604188800000).Month
Result on my Dutch system:
11

There is no reason to involve string representations of timestamps; apart from slowing things down, there's the pitfall of culture-specific interpretation, as you've experienced (see next section):
The simplest solution is to use type [datetimeoffset] (System.DateTimeOffset):
# Returns 11 - the month index - in all time zones *at or ahead of* UTC.
[datetimeoffset]::FromUnixTimeMilliseconds(1604188800000).LocalDateTime.Month
A corrected version of your solution attempt:
[timezone]::CurrentTimeZone.ToLocalTime(
([datetime] '1/1/1970').AddMilliSeconds(1604188800000)
).Month
Note that the from-string conversion from '1/1/1970' is unproblematic in this case, due to using a cast.
You didn't need a call to Get-Date at all, which is where the problem arose due to passing a string representation of a date - see next section.
Incidentally, Get-Date -Date directly accepts a [datetime] instance, so there's no need to pass a string.
When you do pass a string, the resulting [datetime] (System.DateTime) has a .Kind property value of Unspecified rather than Local
Specifically, you've run into a problematic inconsistency in from-string type conversions in different contexts:
Casts and script/function parameters use culture-invariant from-string conversion; that is, irrespective of what the current culture is (as reflected in $PSCulture), the rules of the so-called invariant culture are used, whose date formats are based on the US-English culture; therefore, for instance, [datetime] '11/10/2000' is always interpreted as "10 November 2000", i.e., the first token, 11, is interpreted as the month.
However, more international-friendly formats are supported too; e.g.,
[datetime] '2000-11-10' is the equivalent of the above.
Unexpectedly, cmdlet parameters use culture-sensitive from-string conversion.
This discrepancy is a known problem, but it was decided not to fix it, so as not to break backward compatibility - see GitHub issue #3348.

Related

Powershell cast string to time only

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.

Powershell what is the time format

2017-05-05 12:03:02 -0400 (Fri, 05 May 2017)
what time format is the above in powershell??
i need the same format to compare with today time in powershell.
Get-Date -format
Since this is a follow-up question, you should mention this.
To convert a string containing a datetime based on UTC with a timezone offset,
use [datetime]::ParseExact(). You should change the RegEx from previous Q&A to only contain the datetime without the redundant information in parentheses.
[datetime]::ParseExact("2017-05-05 12:03:02 -0400","yyyy-MM-dd HH:mm:ss zzzzz",$Null)
This will convert to local time (and correctly include the DST aka daylight saving time at that date)
Sample output here with timezone +1 and DST active to:
2017-05-05 18:03:02 +02:00
Or more universal with .ToString('U') and my German locale settings to:
Freitag, 5. Mai 2017 16:03:02
To get the difference to [datetime]::Now simply subtract and choose a property from
> [datetime]::Now - [datetime]::ParseExact("2017-05-05 12:03:02 -0400","yyyy-MM-dd HHH:mm:ss zzzzz",$Null)
Days : 593
Hours : 20
Minutes : 15
Seconds : 35
Milliseconds : 615
Ticks : 513081356156056
TotalDays : 593,844162217657
TotalHours : 14252,2598932238
TotalMinutes : 855135,593593427
TotalSeconds : 51308135,6156056
TotalMilliseconds : 51308135615,6056
You can build more or less any format you like using the DateTime Format Strings and either the format operator (-f) or the ToString() method. For example:
(Get-Date).ToString("yyyy-MM-dd hh:mm:ss")
Which gives something like this:
2018-12-20 12:50:53
If you want to go the other way, you can do it like this:
Get-Date "2017-05-05 12:03:02 -0400"
This converts your string to an actual DateTime object, which can be directly compared to any other DateTime. So, to check if your target date is before now, for example, do something like this:
(Get-Date "2017-05-05 12:03:02 -0400") -lt (Get-Date)

Odd Date/Time format

I have a single program in the Uninstalls registry key with an odd date/time. The value reported is '1533407816', but the actual date is most likely '20180804', which is what all the other Autodesk products installed that day report as.
I am using [datetime]::ParseExact($program.GetValue('InstallDate'),'yyyyMMdd',$null) to convert those dates to a proper DateTime, but not sure what mechanism I need for the odd one. Or if it's even meaningful data.
The value is likely a Unix time stamp, which is the number of seconds that have elapsed since midnight (UTC) of 1 Jan 1970.
You can convert this value to a [datetime] instance in local time as follows:
[DateTimeOffset]::FromUnixTimeSeconds(1533407816).LocalDateTime
To get a UTC [datetime] instance instead, use .UtcDateTime.
Alternatively, work directly with the [System.DateTimeOffset] instance returned by [DateTimeOffset]::FromUnixTimeSeconds().
Note:
Unix time stamps are signed 32-bit integers ([int]) in seconds.
As an aside, this means that the latest date that they can represent is in the year 2038, a limitation known as the Year 2038 problem.
By contrast, [datetime] instances have a resolution of 100 nanoseconds, called ticks, stored in the .Ticks property, whose type is [long] ([System.Int64] a signed 64-bit integer). To manually convert seconds to ticks, multiply them with 10000000L (1e7L).
Simple helper function:
function convertFrom-UnixTime {
param([int] $Value)
[DateTimeOffset]::FromUnixTimeSeconds($Value).LocalDateTime
}
In action, in the Eastern Time Zone, using the en-US culture:
PS> convertFrom-UnixTime 1533407816
Saturday, August 4, 2018 2:36:56 PM
Expressed in Coordinated Universal Time (UTC) (en-US culture):
PS> (convertFrom-UnixTime 1533407816).ToUniversalTime()
Saturday, August 4, 2018 6:36:56 PM
That odd value to me looks like a Unix Timestamp.
Here's a function that will convert it to a normal date
function ConvertFrom-UnixTimeStamp([Int64]$unixTimeStamp, [switch]$asUtc) {
if ($unixTimeStamp -gt 253402430399) {
Write-Error "Cannot convert $unixTimeStamp to DateTime. Only integer values up to 253402430399 are valid."
}
elseif ($unixTimeStamp -gt [Int32]::MaxValue) {
Write-Warning "The given value exceeds the [Int32]::MaxValue and therefore enters the Year2038 Unix bug.."
}
[DateTime]$origin = New-Object System.DateTime 1970, 1, 1, 0, 0, 0, 0, Utc
if ($asUtc) { return $origin.AddSeconds($unixTimeStamp) }
return ($origin.AddSeconds($unixTimeStamp)).ToLocalTime()
}
ConvertFrom-UnixTimeStamp 1533407816 -asUtc will yield a DateTime object:
Saturday, August 4, 2018 18:36:56

Retrieve datetime object in Powershell without the time portion

Is it possible to retrieve only the date portion of a datetime object in PowerShell? Reason being I need to compare the LastWriteTime property of files with today's date to determine whether to backup a file or not. As-is a datetime object includes the time as well which will always evaluate to false when I do something like:
if ($fileDate -eq $currentDate) {
# Do backup
}
I haven't found anyway to do this. If we use the format operator or a method, it converts the object to a string object. If you try to convert that back to a datetime object, it appends the time back onto the object. Probably something simple, but I've been looking at this script for a while and that's the last part that's breaking.
EDIT: As #jessehouwing points out in the comments below, my answers are unnecessarily complicated. Just use $datetime.Date.
A couple of ways to get a DateTime without any time component (ie set to midnight at the start of the date in question, 00:00:00):
$dateTime = <some DateTime>
$dateWithoutTime = $dateTime.AddSeconds(-$dateTime.Second).AddMinutes(-$dateTime.Minute).AddHours(-$dateTime.Hour)
or
$dateTime = <some DateTime>
$dateWithoutTime = Get-Date -Date ($dateTime.ToString("yyyy-MM-dd"))
I ran each version in a loop, iterating 100,000 times. The first version took 16.4 seconds, the second version took 26.5 seconds. So I would go with the first version, although it looks a little more complicated.
Based on answers found here: https://techibee.com/powershell/powershell-how-to-query-date-time-without-seconds/2737 (that article is about stripping just the seconds from a DateTime. But it can be extended to stripping hours, minutes and seconds).
Assuming $fileDate is not a dateTime object, then you can just convert both to strings and format them.
if ($fileDate.ToString() -eq $currentDate.ToString("dd/MM/yyyy")) {
# Do backup
}
This will not answer how to remove time on datetime, but to do your validation purpose of identifying when to backup.
I do suggest to subtract your two given date values and compare the result if total hours are already met to do your backup.
if (( $currentDate - $fileDate ).TotalDays > 7) {
# Do Backup
}
you can also validate for the following
Days :
Hours :
Minutes :
Seconds :
Milliseconds :
Ticks :
TotalDays :
TotalHours :
TotalMinutes :
TotalSeconds :
TotalMilliseconds :
Assuming $currentTime contains a DateTime object, you can retrieve a new DateTime object with the same date but with the time portion zeroed like this:
$midnight = Get-Date $currentTime -Hour 0 -Minute 0 -Second 0 -Millisecond 0

How to get last day of specified month in powershell

I need to get the last day of a month previous from specified.
I have a string (file name), which contains a date in its end. I need to capture the date (already done) and get the last date of the preceding month. For example the string is "proemail vytvoreni_9.2017 2017-10-16", so I need to get 30th September 2017. This is what I have now:
$Report = Read-Host "File name"
$Date = [datetime]$Report.Substring($Report.get_Length()-10)
$Last_month = $Date.AddMonths(-1)
$Date_text = $Last_month.ToString().Substring(3,7)
$month_year = ($Date_text.Split("."))
$days_count = [datetime]::DaysInMonth($month_year[1],$month_year[0])
$days_count = $days_count.ToString()
$month = $month_year[0]
$year = $month_year[1]
$Date_limit = [DateTime]($month,$days_count,$year)
All works well, except for the last row, that returns this error: Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.DateTime". I tried to convert $month and $year to string by .ToString() method, but it didn't help
(Get-Date).AddDays(-$(Get-Date).Day)
Saturday, September 30, 2017 2:36:19 PM
((Get-Date).AddDays(-$(Get-Date).Day)).Day
30
$filename = "proemail vytvoreni_9.2017 2017-10-16"
# Take the date from the filename
$sub = $filename.Substring($filename.Length-10)
# make it into a date format
$filedate = Get-Date -Date $sub
# take that date 2017-10-16 and substracts its own days so in this case substract 16 days, then show me the date
(($filedate).AddDays(-$filedate.Day)).Date
Output:
Saturday 30 september 2017 0:00:00
I've looked at this problem and different solution for this for awhile and here is my answer.
Create any date you want. Here is an easy way to get the month you want.
$date = [datetime]::Parse("11/2019")
Then simply add the next month and remove one day. p
$date.AddMonths(1).AddDays(-1)
Hope this helps somebody else.