From 4e2e05c1b9db6c9d59c865520d2add4afa6f0fa6 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 7 Sep 2026 16:27:22 +0100 Subject: [PATCH] length: count runes, not bytes MinLength/MaxLength measured len(value), so a string of 8 accented characters (9+ bytes) failed MaxLength(8) despite the error message promising "characters". Count with utf8.RuneCountInString so the check matches the wording and the common-sense intent. Still not grapheme-cluster aware (combining marks, emoji ZWJ sequences count as several runes), which is out of scope for a stdlib-only helper. Co-Authored-By: Claude Sonnet 5 --- length.go | 10 ++++++---- length_test.go | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/length.go b/length.go index 02d77f6..22987cd 100644 --- a/length.go +++ b/length.go @@ -1,15 +1,17 @@ package validate +import "unicode/utf8" + var ( ErrMustBeLonger = NewError("must contain at least %d characters") ErrMustBeShorter = NewError("must contain no more than %d characters") ) // MaxLength validates that a string is no longer than a given maximum. -// Length is measured in bytes (len), not runes. +// Length is counted in runes, so multi-byte characters count as one. func MaxLength(l int) func(string) error { return func(value string) error { - if len(value) > l { + if utf8.RuneCountInString(value) > l { return ErrMustBeShorter.With(l) } return nil @@ -17,10 +19,10 @@ func MaxLength(l int) func(string) error { } // MinLength validates that a string is at least a given minimum length. -// Length is measured in bytes (len), not runes. +// Length is counted in runes, so multi-byte characters count as one. func MinLength(l int) func(string) error { return func(value string) error { - if len(value) < l { + if utf8.RuneCountInString(value) < l { return ErrMustBeLonger.With(l) } return nil diff --git a/length_test.go b/length_test.go index defd24e..b471c38 100644 --- a/length_test.go +++ b/length_test.go @@ -20,7 +20,14 @@ func ExampleMinLength() { func TestMaxLength(t *testing.T) { testCases := map[int]map[string]error{ - 8: {"abcd": nil, "abcdefgh": nil, "abcd efg": nil, "abcdefghi": ErrMustBeShorter.With(8)}, + 8: { + "abcd": nil, + "abcdefgh": nil, + "abcd efg": nil, + "abcdéfgh": nil, // 8 runes, 9 bytes + "abcdefghi": ErrMustBeShorter.With(8), + "abcdéfghi": ErrMustBeShorter.With(8), // 9 runes, 10 bytes + }, } for setup, values := range testCases { @@ -41,7 +48,14 @@ func TestMaxLength(t *testing.T) { func TestMinLength(t *testing.T) { testCases := map[int]map[string]error{ - 8: {"abcd": ErrMustBeLonger.With(8), "abcdefgh": nil, "abcd efg": nil, "abcdefghi": nil}, + 8: { + "abcd": ErrMustBeLonger.With(8), + "abcdéfg": ErrMustBeLonger.With(8), // 7 runes, 8 bytes + "abcdefgh": nil, + "abcdéfgh": nil, // 8 runes, 9 bytes + "abcd efg": nil, + "abcdefghi": nil, + }, } for setup, values := range testCases {