1
\$\begingroup\$

The title says it all, I have written two scripts to calculate dates to test my abilities in programming, one from scratch, without [DateTime], New-TimeSpan and even [math]::Ceiling, and it's really long and complex, and there is a max error of one inherited from algorithm, the other uses builtin and it's significantly shorter than the former, I use PowerShell 7.1 x64 on Windows 10, confirmed they are both working, though I haven't tested them on lower versions of PowerShell.

The purpose of the question is to shorten their code, make them more efficient without changing their logic and method, feel free to contribute if you think you can improve them, now I will post them below:

Without Built-in:

    $Culture=@(
[PSCustomObject]@{Format='yyyy-MM-dd';Regex='\d{4}\-0?([2-9]|1[0-2]?)\-(0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='yyyy/MM/dd';Regex='\d{4}\/0?([2-9]|1[0-2]?)\/(0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='MM/dd/yyyy';Regex='0?([2-9]|1[0-2]?)\/(0?(3[01]|[12][0-9]|[1-9]))\/\d{4}'}
[PSCustomObject]@{Format='MMM dd, yyyy';Regex='[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), \d{4}'}
[PSCustomObject]@{Format='dd MMM, yyyy';Regex='(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, \d{4}'}
[PSCustomObject]@{Format='MMMM dd, yyyy';Regex='[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), \d{4}'}
[PSCustomObject]@{Format='dd MMMM, yyyy';Regex='(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, \d{4}'}
[PSCustomObject]@{Format='yyyy, MMM dd';Regex='\d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='yyyy, MMMM dd';Regex='\d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))'}
)

$months=@(
[PSCustomObject]@{Month='January';Days=31;Number=1}
[PSCustomObject]@{Month='February';Days=28;Number=2}
[PSCustomObject]@{Month='March';Days=31;Number=3}
[PSCustomObject]@{Month='April';Days=30;Number=4}
[PSCustomObject]@{Month='May';Days=31;Number=5}
[PSCustomObject]@{Month='June';Days=30;Number=6}
[PSCustomObject]@{Month='July';Days=31;Number=7}
[PSCustomObject]@{Month='August';Days=31;Number=8}
[PSCustomObject]@{Month='September';Days=30;Number=9}
[PSCustomObject]@{Month='October';Days=31;Number=10}
[PSCustomObject]@{Month='November';Days=30;Number=11}
[PSCustomObject]@{Month='December';Days=31;Number=12}
)

function Identify {
    PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] [System.String] $string, 
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] [System.String] $regex
        )
        $string -eq ($string| Select-String -pattern $regex).matches.value
    }
    
function Leapdays {
    PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $date
        )
        Process {
            [int]$years=$date.year
            if ($date.month -le 2) { $years-=1}
            return $years / 4 - $years / 100 + $years / 400
        }
    }
    
function ParseDate {
    Param(
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $string,
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] $format
    )
    
    if ($format -eq 'yyyy-MM-dd'){
    $Year=$string.Split("-")[0] -as [int]
    $Month=$string.Split("-")[1] -as [int]
    $Day=$string.Split("-")[2] -as [int]
    }
    elseif ($format -eq 'yyyy/MM/dd'){
    $Year=$string.Split("/")[0]  -as [int]
    $Month=$string.Split("/")[1]  -as [int]
    $Day=$string.Split("/")[2] -as [int]
    }
    elseif ($format -eq 'MM/dd/yyyy'){
    $Year=$string.Split("/")[2] -as [int]
    $Month=$string.Split("/")[0] -as [int]
    $Day=$string.Split("/")[1] -as [int]
    }
    elseif ($format -eq 'MMM dd, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[0]+"*"}).number -as [int]
    $Day=$string.Split( )[1] -as [int]
    }
    elseif ($format -eq 'dd MMM, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[1]+"*"}).number -as [int]
    $Day=$string.Split( )[0] -as [int]
    }
    elseif ($format -eq 'MMMM dd, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[0]}).number -as [int]
    $Day=$string.Split( )[1] -as [int]
    }
    elseif ($format -eq 'dd MMMM, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[1]}).number -as [int]
    $Day=$string.Split( )[0] -as [int]
    }
    elseif ($format -eq 'yyyy, MMM dd'){
    $string=$string -replace ","
    $Year=$string.Split( )[0] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[1]+"*"}).number -as[int]
    $Day=$string.Split( )[2] -as [int]
    }
    elseif ($format -eq 'yyyy, MMMM dd'){
    $string=$string -replace ","
    $Year=$string.Split( )[0] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[1]}).number -as [int]
    $Day=$string.Split( )[2] -as [int]
    }
    [PSCustomObject]@{Year=$Year;Month=$Month;Day=$Day}
    }

function Diff-Date {
    PARAM(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $date1, 
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] $date2
        )
        
    for ($i=0;$i -lt $Culture.count;$i++) {
        if (Identify $date1 $Culture[$i].Regex) {
            $date1=ParseDate $date1 $Culture[$i].Format
            break
        }
    }
    for ($i=0;$i -lt $Culture.count;$i++) {
        if (Identify $date2 $Culture[$i].Regex) {
            $date2=ParseDate $date2 $Culture[$i].Format
            break
        }
    }
    $month1=0
    $month2=0
    for ($i=0;$i -lt $date1.month-1;$i++) {
        $month1+=$months[$i].Days
    }
    for ($i=0;$i -lt $date2.month-1;$i++) {
        $month2+=$months[$i].Days
    }
    $totaldays1=$date1.year*365+$(Leapdays $date1)+$month1+$date1.day
    $totaldays2=$date2.year*365+$(Leapdays $date2)+$month2+$date2.day
    $diffdate = $totaldays2 - $totaldays1
    if ($diffdate -lt [int]$diffdate) {$diffdate = [int]$diffdate}
    elseif ($diffdate -gt [int]$diffdate) {$diffdate = [int]$diffdate+1}
    return $diffdate
}
$date1=Read-Host "Input Start Date"
if (-not $date1) {
    $date1="1999-04-10"
}
Write-Host $date1
$date2=Read-Host "Input End Date"
if (-not $date2) {
    $date2=(Get-Date -DisplayHint Date -Format "yyyy-MM-dd" | Out-String).TrimEnd()
}
Write-Host $date2
Diff-Date $date1 $date2

With Builtin

function D-Days {
PARAM(
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $date1, 
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] $date2
    )
    
$date1=[DateTime]$date1
$date2=[DateTime]$date2
$ddate=New-TimeSpan -Start $date1 -End $date2
$date1=$date1.ToString("MMMM dd, yyyy")
$date2=$date2.ToString("MMMM dd, yyyy")
$length=([string]$ddate.days).length
$ddate=$ddate.ToString("d"*$length)
Write-Output "Start date is: $date1, End date is: $date2, Date difference is: $ddate days"
}
$date1=Read-Host "Input Start Date"
if (-not $date1) {
    $date1="1999-04-10"
}
Write-Host $date1
$date2=Read-Host "Input End Date"
if (-not $date2) {
    $date2=(Get-Date -DisplayHint Date -Format "yyyy-MM-dd" | Out-String).TrimEnd()
}
Write-Host $date2
D-Days $date1 $date2

Major Update: I have revised the leapdays function used to count leap days before date, in the script without builtin, it completely eliminated the max error of 1 day, by output an integer leap days to any input, like how leap days work in reality, the idea is very simple, four/4-century/100+cycle/400, where four means nearest multiple of four less than or equal to year, century is century of that year (2000 is the end of 20th century, sorry but the start of 21 century is 2001 and so on...because there is no A.D. 0), and cycle is nearest multiple of 400 less than or equal to year, now I know of [math]::floor to get only quotient, but I decided against using it in the script without built-in, so here is the script:

    $Culture=@(
[PSCustomObject]@{Format='yyyy-MM-dd';Regex='\d{4}\-0?([2-9]|1[0-2]?)\-(0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='yyyy/MM/dd';Regex='\d{4}\/0?([2-9]|1[0-2]?)\/(0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='MM/dd/yyyy';Regex='0?([2-9]|1[0-2]?)\/(0?(3[01]|[12][0-9]|[1-9]))\/\d{4}'}
[PSCustomObject]@{Format='MMM dd, yyyy';Regex='[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), \d{4}'}
[PSCustomObject]@{Format='dd MMM, yyyy';Regex='(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, \d{4}'}
[PSCustomObject]@{Format='MMMM dd, yyyy';Regex='[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), \d{4}'}
[PSCustomObject]@{Format='dd MMMM, yyyy';Regex='(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, \d{4}'}
[PSCustomObject]@{Format='yyyy, MMM dd';Regex='\d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))'}
[PSCustomObject]@{Format='yyyy, MMMM dd';Regex='\d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))'}
)

$months=@(
[PSCustomObject]@{Month='January';Days=31;Number=1}
[PSCustomObject]@{Month='February';Days=28;Number=2}
[PSCustomObject]@{Month='March';Days=31;Number=3}
[PSCustomObject]@{Month='April';Days=30;Number=4}
[PSCustomObject]@{Month='May';Days=31;Number=5}
[PSCustomObject]@{Month='June';Days=30;Number=6}
[PSCustomObject]@{Month='July';Days=31;Number=7}
[PSCustomObject]@{Month='August';Days=31;Number=8}
[PSCustomObject]@{Month='September';Days=30;Number=9}
[PSCustomObject]@{Month='October';Days=31;Number=10}
[PSCustomObject]@{Month='November';Days=30;Number=11}
[PSCustomObject]@{Month='December';Days=31;Number=12}
)

function Identify {
    PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] [System.String] $string, 
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] [System.String] $regex
        )
        $string -eq ($string| Select-String -pattern $regex).matches.value
    }
    
function Leapdays {
    PARAM (
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $date
        )
        Process {
            [int]$years=$date.year
            if ($date.month -le 2) { $years-=1}
            if ($years % 4 -eq 0) {$leap=$years/4}
            else {while ($years % 4 -ne 0) {$years-=1}
            $leap=$years/4}
            if ($years % 100 -eq 0) {$century=$years/100}
            else {while ($years % 100 -ne 0) {$years-=4}
            $century=$years/100}
            if ($years % 400 -eq 0) {$cycle=$years/400}
            else {while ($years % 400 -ne 0) {$years-=100}
            $cycle=$years/400}
            $leap=$leap-$century+$cycle
            $leap
        }
    }
    
function ParseDate {
    Param(
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $string,
    [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] $format
    )
    
    if ($format -eq 'yyyy-MM-dd'){
    $Year=$string.Split("-")[0] -as [int]
    $Month=$string.Split("-")[1] -as [int]
    $Day=$string.Split("-")[2] -as [int]
    }
    elseif ($format -eq 'yyyy/MM/dd'){
    $Year=$string.Split("/")[0]  -as [int]
    $Month=$string.Split("/")[1]  -as [int]
    $Day=$string.Split("/")[2] -as [int]
    }
    elseif ($format -eq 'MM/dd/yyyy'){
    $Year=$string.Split("/")[2] -as [int]
    $Month=$string.Split("/")[0] -as [int]
    $Day=$string.Split("/")[1] -as [int]
    }
    elseif ($format -eq 'MMM dd, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[0]+"*"}).number -as [int]
    $Day=$string.Split( )[1] -as [int]
    }
    elseif ($format -eq 'dd MMM, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[1]+"*"}).number -as [int]
    $Day=$string.Split( )[0] -as [int]
    }
    elseif ($format -eq 'MMMM dd, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[0]}).number -as [int]
    $Day=$string.Split( )[1] -as [int]
    }
    elseif ($format -eq 'dd MMMM, yyyy'){
    $string=$string -replace ","
    $Year=$string.Split( )[2] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[1]}).number -as [int]
    $Day=$string.Split( )[0] -as [int]
    }
    elseif ($format -eq 'yyyy, MMM dd'){
    $string=$string -replace ","
    $Year=$string.Split( )[0] -as [int]
    $Month=($months | Where-Object{$_.Month -like $string.split( )[1]+"*"}).number -as[int]
    $Day=$string.Split( )[2] -as [int]
    }
    elseif ($format -eq 'yyyy, MMMM dd'){
    $string=$string -replace ","
    $Year=$string.Split( )[0] -as [int]
    $Month=($months | Where-Object{$_.Month -eq $string.split( )[1]}).number -as [int]
    $Day=$string.Split( )[2] -as [int]
    }
    [PSCustomObject]@{Year=$Year;Month=$Month;Day=$Day}
    }

function Diff-Date {
    PARAM(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] $date1, 
        [Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] $date2
        )
        
    for ($i=0;$i -lt $Culture.count;$i++) {
        if (Identify $date1 $Culture[$i].Regex) {
            $date1=ParseDate $date1 $Culture[$i].Format
            break
        }
    }
    for ($i=0;$i -lt $Culture.count;$i++) {
        if (Identify $date2 $Culture[$i].Regex) {
            $date2=ParseDate $date2 $Culture[$i].Format
            break
        }
    }
    $month1=0
    $month2=0
    for ($i=0;$i -lt $date1.month-1;$i++) {
        $month1+=$months[$i].Days
    }
    for ($i=0;$i -lt $date2.month-1;$i++) {
        $month2+=$months[$i].Days
    }
    $totaldays1=($date1.year-1)*365+$(Leapdays $date1)+$month1+$date1.day
    $totaldays2=($date2.year-1)*365+$(Leapdays $date2)+$month2+$date2.day
    $diffdate = $totaldays2 - $totaldays1
    return $diffdate
}
$date1=Read-Host "Input Start Date"
if (-not $date1) {
    $date1="1999-04-10"
}
Write-Host $date1
$date2=Read-Host "Input End Date"
if (-not $date2) {
    $date2=(Get-Date -DisplayHint Date -Format "yyyy-MM-dd" | Out-String).TrimEnd()
}
Write-Host $date2
Diff-Date $date1 $date2
\$\endgroup\$
3
  • 2
    \$\begingroup\$ in the 2nd code section ... why are you using date strings instead of keeping everything as datetime objects? you really otta not EVER use date strings unless they are absolutely needed. stay with date objects until you need to do otherwise ... and even then, try to only use data strings for display. \$\endgroup\$
    – Lee_Dailey
    Dec 4 '20 at 9:34
  • \$\begingroup\$ @Lee_Dailey Well, I thought my code is clear and comprehensive enough, I only need New-TimeSpan to make the code working, but you see I also use Write-Output, the thing is I find the output of Write-Output line awkward and ugly if I don't format the variables, and I really didn't find a way to format [datetime] and [timespan]... \$\endgroup\$ Dec 4 '20 at 9:42
  • 2
    \$\begingroup\$ you can format things for display as needed. something like $TimeStamp.ToString('yyyy-MM-dd') with as much detail as you need. just keep the objects as datetime objects for any other use. only go to datetime strings when you want to display things or send them to a file. \$\endgroup\$
    – Lee_Dailey
    Dec 4 '20 at 10:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.