Domain-specific validation for golf handicaps
2 July 2022
A user in Germany types “22,4” into a handicap field and the form tells them it’s invalid. They’re right and the form is wrong. That comma is a decimal separator, not a typo.
I hit this building a registration platform for a golf tournament series. The handicap field needed to accept values from -4 to +54, with at most one decimal place, where users might type a comma or a period depending on their locale. Standard number validators — min, max, between — couldn’t handle any of this because they assume a period and reject anything that doesn’t parse to a clean number.
The constraints are specific enough that I needed a custom rule. Here’s what the validation has to handle:
- The value might use a comma as the decimal separator (European format)
- Only one decimal place is allowed — “22,45” is invalid even though it’s a valid number
- The range is -4 to +54
- Standard
Number()parsing chokes on commas
The custom VeeValidate rule I wrote:
extend('between_decimal', {
validate(value, { min, max }) {
// Reject more than one decimal place
if (value.split('.').length > 1) return false
// Normalise European decimal separator
value = value.replace(',', '.')
value = Number(value)
return Number(min) <= value && Number(max) >= value
},
params: ['min', 'max'],
})
Usage in the template:
<TextInput
name="handicap"
:label="$t('form.handicap')"
rules="required|between_decimal:-4,54"
/>
A few things about this that are easy to miss.
The decimal place check uses split('.') on the original string, before the comma replacement. This catches “22.45” but not “22,4” — which is correct, because “22,4” is one decimal place in European notation. After normalising the comma to a period, Number("22.4") parses fine and the range check works.
The rule treats the value as a string first and a number second. That ordering matters. If I’d parsed to a number immediately, “22,4” would become NaN and the range check would fail silently. String manipulation first, type coercion second.
I also considered using parseFloat instead of Number(), but parseFloat("22,4") returns 22 — it stops at the comma. Number() after comma replacement is the safer path.
This rule handled three tournament formats where the handicap field appeared in different form steps with different labels but the same validation. One rule, applied everywhere, and the domain logic lives in one place.
It’s the kind of validation that sounds trivial until someone in France types “22,4” and your form says no.