modules/Helper/DateTimeFormula.psm1

class DateTimeFormula{
    hidden [string] $referentUnit
    hidden [bool] $isReferentNegative
    hidden [int] $referentValue
    hidden [string] $timespanUnit
    hidden [bool] $isFormula

    [string] $formula
    [datetime] $datetime

    DateTimeFormula([string]$formula){
        $this.formula = $formula
        $this.identifyUnits()

        try {
            $this.datetime = $this.parseDateTime()
        }
        catch {
            throw ("Invalid date value: $formula")
        }
    }

    [datetime] applyTimeSpanFormula([string]$formula){
        return $this.applyTimeSpanFormula($this.datetime, $this.splitDateFormula($formula))
    }

    hidden [void] identifyUnits() {
        if ($this.formula.SubString(0,1) -eq '-') {
            $this.isReferentNegative = $true
            $this.formula = $this.formula.SubString(1)
        }
        else {
            $this.isReferentNegative = $false
        }
        if ($this.formula.SubString(0, 1) -cin ('Y', 'M', 'D', 'h', 'm')) {
            $this.referentUnit = $this.formula.SubString(0, 1)

            $haveValue = $false
            for($i=1; $i -lt $this.formula.Length; $i++){
                if ($this.formula[$i] -cin ('Y', 'Q', 'M', 'D', 'W', 'D', 'h', 'm', 's')){
                    $this.referentValue = $this.formula.SubString(1, $i)
                    $this.timespanUnit = $this.formula.SubString($i)
                    $haveValue = $true
                    break
                }
            }
            if (-not $haveValue){
                $this.referentValue = $this.formula.SubString(1)
                $this.timespanUnit = ''
            }

            $this.isFormula = $true
        }
        elseif ($this.formula.SubString(0, 1) -ceq 'C') {
            $this.referentUnit = $this.formula.SubString(0, 2)
            $this.timespanUnit = $this.formula.SubString(2)

            $this.isFormula = $true
        }
        elseif ($this.formula.SubString(0, 2) -ceq 'WD') {
            $this.referentUnit = $this.formula.SubString(0, 2)

            if ($this.formula.Length -gt 2){
                $this.referentValue = $this.formula.SubString(2, 1)
                $this.timespanUnit = $this.formula.SubString(3)
            }
            else {
                $this.referentValue = '0'
                $this.timespanUnit = ''
            }

            $this.isFormula = $true
        }
        else {
            $this.isFormula = $false
        }
    }

    hidden [datetime] parseDateTime(){
        $retValue = $null

        if ($this.isFormula) {
            $retValue = $this.getReferentDate($this.referentUnit)

            if ($this.timespanUnit) {
                $dt = $this.splitDateFormula($this.timespanUnit)
                $retValue = $this.applyTimeSpanFormula($retValue, $dt)
            }
            else {
                return $retValue
            }
        }
        else {
            try {
                $retValue = [datetime]::parse($this.formula)
            }
            catch {
                throw ('Invalid date formula: {0}' -f @($this.formula, $_))
            }
        }

        return $retValue
    }

    hidden [datetime] applyTimeSpanFormula([datetime]$datetime, [PSCustomObject]$exp){
        $retValue = $datetime

        foreach ($node in $exp.nodes) {
            switch -CaseSensitive ($node.unit) {
                'Y' { $retValue = $retValue.AddYears.($node.number) }
                'Q' { $retValue = $retValue.AddMonths(3 * $node.number) }
                'M' { $retValue = $retValue.AddMonths($node.number) }
                'W' { $retValue = $retValue.AddDays(7 * $node.number) }
                'D' { $retValue = $retValue.AddDays($node.number) }
                'h' { $retValue = $retValue.AddHours($node.number) }
                'm' { $retValue = $retValue.AddMinutes($node.number) }
                's' { $retValue = $retValue.AddSeconds($node.number) }
            }
        }

        return $retValue
    }

    hidden [datetime] getReferentDate([string]$value){
        $date = Get-Date

        switch -CaseSensitive ($value){
            'CT' { $date = (Get-Date) }
            'CY' { $date = (Get-Date -Year $date.Year -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0) }
            'CQ' { $date = (Get-Date -Year $date.Year -Month (3 * [Math]::Floor($date.Month / 3)) -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0) }
            'CM' { $date = (Get-Date -Year $date.Year -Month $date.Month -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0) }
            'CW' { $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddDays(-$date.DayOfWeek) }
            'CD' { $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour 0 -Minute 0 -Second 0 -Millisecond 0) }
            'Ch' { $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $date.Hour -Minute 0 -Second 0 -Millisecond 0) }
            'Cm' { $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $date.Hour -Minute $date.Minute -Second 0 -Millisecond 0) }
            'Y' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddYears(-$this.referentValue)
                }
                else {
                    $date = (Get-Date -Year $this.referentValue -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0)
                }
            }
            'M' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddMonths(-$this.referentValue)
                }
                else {
                    $date = (Get-Date -Year $date.Year -Month $this.referentValue -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0)
                }
            }
            'D' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddDays(-$this.referentValue)
                }
                else {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $this.referentValue -Hour 0 -Minute 0 -Second 0 -Millisecond 0)
                }
            }
            'h' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $date.Hour -Minute 0 -Second 0 -Millisecond 0).AddHours(-$this.referentValue)
                }
                else {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $this.referentValue -Minute 0 -Second 0 -Millisecond 0)
                }
             }
            'm' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $date.Hour -Minute $date.Minute -Second 0 -Millisecond 0).AddMinutes(-$this.referentValue)
                }
                else {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour $date.Hour -Minute $this.referentValue -Second 0 -Millisecond 0)
                }
             }
            'WD' {
                if ($this.isReferentNegative) {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddDays(-7).AddDays(-($date.DayOfWeek-$this.referentValue))
                }
                else {
                    $date = (Get-Date -Year $date.Year -Month $date.Month -Day $date.Day -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddDays(-($date.DayOfWeek-$this.referentValue))
                }
            }
            default { $date = $null }
        }

        return $date
    }

    hidden [PSCustomObject] splitDateFormula([string] $value){
        $retValue = [PSCustomObject]@{expression=$value; nodes=@()}

        $prev=0
        for($i=0; $i -lt $value.Length; $i++){
            if ($value[$i] -cin ('Y', 'Q', 'M', 'D', 'W', 'D', 'h', 'm', 's')){
                $number = $value.Substring($prev, $i-$prev)
                $unit = $value.Substring($i, 1)
                $prev = $i + 1
                $retValue.nodes += [PSCustomObject]@{number=[int]$number; unit=$unit}
            }
        }

        return $retValue
    }
}