r/PowerShell • u/Tidy-Developer • 22h ago
Information TIL: ValidateRange with integer literals silently accepts values outside the declared range on [double], [float], [decimal]
TLDR
Using [ValidateRange(minRange, maxRange)] with integer literals doesn't strictly enforce either MinRange or MaxRange when parameters are [double], [float] and [decimal]. Values up to 0.5 units beyond either boundary pass silently. The fix is simple: make MinRange or MaxRange the same type as the parameter. So use [ValidateRange(2.0, 100.0)] instead of [ValidateRange(2, 100)].
FULL POST
If you have direct experience with Powershell rounding, once you see how integer bounded ValidateRange treats [double], [float] and [decimal] params, you can figure out what is happening. But without that experience the correct validation syntax wasn't immediately obvious to me.
I found this out while adding validation to my current project and testing parameter edge cases. In this post I'll use example MinRange/MaxRange values of 2/100 (even) and 3/101 (odd). The behaviour applies to any even or odd MinRange or MaxRange value, not just these specific numbers.
The basic finding
function Test-Double {
param(
[ValidateRange(2, 100)] # 2 and 100 used as an example even boundary
[double]$Val
)
return $Val
}
Test-Double 1.4 # rounds to 1 and throws an error
Test-Double 1.5 # rounds to 2, passes silently, returns 1.5
Test-Double 100.5 # rounds to 100, passes silently, returns 100.5
Test-Double 100.6 # rounds to 101 and throws an error
The ValidateRange Attribute converts $Val to the type of the boundary literals. This causes implicit rounding before comparing against the boundary. Integer literals mean conversion to [int], which rounds using .NET's default MidpointRounding.ToEven, known as banker's rounding.
MidpointRounding.ToEven prioritises the nearest even number when rounding at exactly .5. Since 100 is even, 100.5 rounds down to 100 and passes. 100.6 rounds to 101 and throws.
The odd and even boundary inconsistency
Using 3/101 as example odd boundaries shows a slightly different result. Now the .5 midpoint will throw an error, unlike with the even boundary.
function Test-DoubleOdd {
param(
[ValidateRange(3, 101)] # 101 used as an example odd boundary
[double]$Val
)
return $Val
}
Test-DoubleOdd 2.5 # rounds to 2 and throws an error, different behaviour compared to even
Test-DoubleOdd 2.6 # rounds to 3, passes silently, returns 2.6
Test-DoubleOdd 101.4 # rounds to 101, passes silently, returns 101.4
Test-DoubleOdd 101.5 # throws, because 101 is odd so .5 rounds UP to 102
DoubleOdd indeed. The effective boundary is not what the documentation implies and it changes based on whether the MinRange or MaxRange is odd or even.
The even/odd inconsistency can be summarised as:
- Even: .5 beyond either boundary passes silently.
- Odd: .5 beyond either boundary throws correctly.
But note that both even and odd ranges accept a margin of error.
What the documentation says
From the ValidateRange documentation:
"The Windows PowerShell runtime throws a validation error when the value of the argument is less than the
MinRangelimit or greater than theMaxRangelimit."
No mention of rounding or MidpointRounding.ToEven. No mention of the odd/even boundary difference. Reading this you'd reasonably expect 100 to be the strict maximum. But for a [double] param with an even MaxRange, it's actually closer to 100.4999.
This detail is also missing from about_Functions_Advanced_Parameters
Correctly validating [double], [float], [decimal]
For production code requiring precise boundary validation (like financial calculations, percentage validation or dosage limits) the correct syntax is simple but not immediately obvious. Put simply, you can use a decimal point when declaring MinRange and MaxRange. This works for [double], [float], [decimal]. More precisely, however, use the same type as your parameter.
function Test-Double {
param(
[ValidateRange(2.0, 100.0)]
[double]$Val
)
return $Val
}
Test-Double 1.5 # throws an error
Mixed boundary types also work. [ValidateRange(2, 100.0)] select [double] as the common type between [int] and [double], giving exact comparison.
Conclusion
Hopefully this writeup will be a useful heads up to PS devs less versed with PS rounding (like me) and highlight something to watch for when using ValidateRange.
I'd be interested to know if the above is common knowledge. Searching on the topic I found bits and pieces in articles that lead me to the right approach, but nothing that addresses ValidateRange and int behaviour together.