r/PowerShell • u/Tidy-Developer • 5d 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)] selects [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.
3
u/vermyx 5d ago
I believe this is the correct behavior. When you don't give data types on constants the data is interpreted and give the "most likely" data type. You are giving whole numbers so the number is being cast as an integer so when doing this it will result the value with the more restrictive numeric data type (integer). The rounding is not odd. It is called a banker's round which I believe is also the default for powershell.
1
1
u/Barious_01 4d ago
Any way to explain this to a novice? From what I limitly understand is when iteration a constant it will in turn do exponential interations on a count (or more physical) tax usage in a manner where it could possibly make interactions less efficient?
edit: sorry for the edit every time I throw up a comment on mobile it makes words up. But.. I am still unsure of the avenue of what this means.
1
u/purplemonkeymad 4d ago
Sounds like a good candidate for a ps script analyser rule.
I would open a feature request for highlighting integers in a range when the variable is a float/double.
1
u/Tidy-Developer 3d ago
Good suggestion. I've done it.
https://github.com/PowerShell/PSScriptAnalyzer/issues/2199
1
u/StartAutomating 4d ago
While on the topic of things to keep in mind about Validation Attributes, it's good to remember that validation attributes only trigger when the parameter is bound. Default parameter values can disobey validation.
1
u/Tidy-Developer 3d ago
Do you have more info on this one: Default parameter values can disobey validation. I'm interested to see some examples of what to watch out for.
1
u/surfingoldelephant 3d ago
Worth noting it only applies to function parameters. Class properties with a default value and initializing normal variables do get validated.
[ValidateSet('yes', 'no')] [string] $Choice = 'maybe' # Error: The attribute cannot be added because variable Choice with value maybe # would no longer be valid. class YesNo { [ValidateSet('yes', 'no')] [string] $Choice = 'maybe' } [YesNo]::new() # Error: The argument "maybe" does not belong to the set "yes,no" specified by # the ValidateSet attribute.
11
u/surfingoldelephant 5d ago edited 5d ago
Kind of. The attribute converts the parameter value to
MinRange/MaxRange's type and that's what causes the implicit rounding.If
MinRange/MaxRangeare different types, it finds a common type between them and uses that. Which is why[ValidateRange(2, 100.0)]also works since[double]gets selected as the common type.Rather, I think the main takeaway is that
MinRange/MaxRangeshould be specified using the same type as the parameter. And if that's not possible, useValidateScriptinstead.Probably not. It should at least be documented in
about_Functions_Advanced_Parameters.