From b07557590afc988a856ef682afa645324e3b0bd0 Mon Sep 17 00:00:00 2001 From: Nikita Galayko Date: Thu, 29 Nov 2018 15:46:52 +0100 Subject: [PATCH 1/3] add benchmarks --- personnummer_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/personnummer_test.go b/personnummer_test.go index 71d1018..be673b8 100644 --- a/personnummer_test.go +++ b/personnummer_test.go @@ -55,3 +55,9 @@ func TestWrongCoOrdinationNumbers(t *testing.T) { assert.False(t, Valid("900161-0017")) assert.False(t, Valid("640893-3231")) } + +func BenchmarkValid(b *testing.B) { + for i := 0; i < b.N; i++ { + Valid("19900101-0017") + } +} From b1f8be514f8692825b2239ab179fa3e1b8ec3555 Mon Sep 17 00:00:00 2001 From: Nikita Galayko Date: Thu, 29 Nov 2018 15:48:46 +0100 Subject: [PATCH 2/3] use only byte operations --- personnummer.go | 186 ++++++++++++++++++++++++------------------- personnummer_test.go | 6 ++ 2 files changed, 110 insertions(+), 82 deletions(-) diff --git a/personnummer.go b/personnummer.go index 5609064..b81f816 100644 --- a/personnummer.go +++ b/personnummer.go @@ -1,121 +1,143 @@ package personnummer -import ( - "math" - "reflect" - "regexp" - "strconv" - "time" -) +import "fmt" -var ( - re = regexp.MustCompile(`^(\d{2}){0,1}(\d{2})(\d{2})(\d{2})([\-|\+]{0,1})?(\d{3})(\d{0,1})$`) +const ( + lengthWithoutCentury = 10 + lengthWithCentury = 12 ) -// luhn will test if the given string is a valid luhn string. -func luhn(str string) int { - sum := 0 - - for i, r := range str { - c := string(r) - v, _ := strconv.Atoi(c) - v *= 2 - (i % 2) - if v > 9 { - v -= 9 +// ValidateStrings validate Swedish social security numbers. +func ValidString(in string) bool { + cleanNumber := make([]byte, 0, len(in)) + for _, c := range in { + if c == '+' { + continue + } + if c == '-' { + continue } - sum += v - } - - return int(math.Ceil(float64(sum)/10)*10 - float64(sum)) -} -// testDate will test if date is valid or not. -func testDate(century string, year string, month string, day string) bool { - t, err := time.Parse("01/02/2006", month+"/"+day+"/"+century+year) + if c > '9' { + return false + } + if c < '0' { + return false + } - if err != nil { - return false + cleanNumber = append(cleanNumber, byte(c)) } - y, _ := strconv.Atoi(century + year) - m, _ := strconv.Atoi(month) - d, _ := strconv.Atoi(day) + switch len(cleanNumber) { + case lengthWithCentury: + if !luhn(cleanNumber[2:]) { + return false + } - if y > time.Now().Year() { + dateBytes := append(cleanNumber[2:6], getCoOrdinationDay(cleanNumber[6:8])...) + return validateTime(dateBytes) + case lengthWithoutCentury: + if !luhn(cleanNumber) { + return false + } + + dateBytes := append(cleanNumber[:4], getCoOrdinationDay(cleanNumber[4:6])...) + return validateTime(dateBytes) + default: return false } +} - return !(t.Year() != y || int(t.Month()) != m || t.Day() != d) +var monthDays = map[byte]byte{ + 1: 31, + 3: 31, + 4: 30, + 5: 31, + 6: 30, + 7: 31, + 8: 31, + 9: 30, + 10: 31, + 11: 30, + 12: 31, } -// getCoOrdinationDay will return co-ordination day. -func getCoOrdinationDay(day string) string { - d, _ := strconv.Atoi(day) - d -= 60 - day = strconv.Itoa(d) +// input time without centry. +func validateTime(time []byte) bool { + date := charsToDigit(time[4:6]) + month := charsToDigit(time[2:4]) - if d < 10 { - day = "0" + day + year := charsToDigit(time[0:2]) + + if month != 2 { + days, ok := monthDays[month] + if !ok { + return false + } + return date <= days } - return day + if year%4 == 0 { + return date <= 29 + } + return date <= 28 } // Valid will validate Swedish social security numbers. -func Valid(str interface{}) bool { - if reflect.TypeOf(str).Kind() != reflect.Int && reflect.TypeOf(str).Kind() != reflect.String { +func Valid(i interface{}) bool { + switch v := i.(type) { + case int, int32, int64, uint, uint32, uint64: + return ValidString(fmt.Sprint(v)) + case string: + return ValidString(v) + default: return false } +} - pr := "" +var rule3 = [...]int{0, 2, 4, 6, 8, 1, 3, 5, 7, 9} - if reflect.TypeOf(str).Kind() == reflect.Int { - pr = strconv.Itoa(str.(int)) - } else { - pr = str.(string) - } +// luhn will test if the given string is a valid luhn string. +func luhn(s []byte) bool { + odd := len(s) & 1 - match := re.FindStringSubmatch(pr) + var sum int - if len(match) == 0 { - return false + for i, c := range s { + if i&1 == odd { + sum += rule3[c-'0'] + } else { + sum += int(c - '0') + } } - century := match[1] - year := match[2] - month := match[3] - day := match[4] - num := match[6] - check := match[7] - - if len(century) == 0 { - yearNow := time.Now().Year() - years := [...]int{yearNow, yearNow - 100, yearNow - 150} + return sum%10 == 0 +} - for _, yi := range years { - ys := strconv.Itoa(yi) +// getCoOrdinationDay will return co-ordination day. +func getCoOrdinationDay(day []byte) []byte { + d := charsToDigit(day) + if d < 60 { + return day + } - if Valid(ys[:2] + pr) { - return true - } - } + d -= 60 - return false + if d < 10 { + return []byte{'0', d + '0'} } - if len(year) == 4 { - year = year[2:] + return []byte{ + d/10 + '0', + d%10 + '0', } +} - c, _ := strconv.Atoi(check) - - valid := luhn(year+month+day+num) == c && len(check) != 0 - - if valid && testDate(century, year, month, day) { - return valid +// charsToDigit converts char bytes to the digit +// example: ['1', '1'] => 11 +func charsToDigit(chars []byte) byte { + if len(chars) == 1 { + return chars[0] - '0' } - - day = getCoOrdinationDay(day) - - return valid && testDate(century, year, month, day) + return (chars[0]-'0')*10 + chars[1] - '0' } diff --git a/personnummer_test.go b/personnummer_test.go index be673b8..772c2ef 100644 --- a/personnummer_test.go +++ b/personnummer_test.go @@ -61,3 +61,9 @@ func BenchmarkValid(b *testing.B) { Valid("19900101-0017") } } + +func BenchmarkValidString(b *testing.B) { + for i := 0; i < b.N; i++ { + ValidString("19900101-0017") + } +} From 34518cb249c14f3c5e60737ecdfb5b425d850608 Mon Sep 17 00:00:00 2001 From: Nikita Galayko Date: Thu, 29 Nov 2018 21:40:36 +0100 Subject: [PATCH 3/3] fix leap year detection --- personnummer.go | 38 ++++++++++++++++++++++++-------------- personnummer_test.go | 7 +++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/personnummer.go b/personnummer.go index b81f816..c854f1a 100644 --- a/personnummer.go +++ b/personnummer.go @@ -34,7 +34,7 @@ func ValidString(in string) bool { return false } - dateBytes := append(cleanNumber[2:6], getCoOrdinationDay(cleanNumber[6:8])...) + dateBytes := append(cleanNumber[:6], getCoOrdinationDay(cleanNumber[6:8])...) return validateTime(dateBytes) case lengthWithoutCentury: if !luhn(cleanNumber) { @@ -48,7 +48,7 @@ func ValidString(in string) bool { } } -var monthDays = map[byte]byte{ +var monthDays = map[int]int{ 1: 31, 3: 31, 4: 30, @@ -64,10 +64,10 @@ var monthDays = map[byte]byte{ // input time without centry. func validateTime(time []byte) bool { - date := charsToDigit(time[4:6]) - month := charsToDigit(time[2:4]) + length := len(time) - year := charsToDigit(time[0:2]) + date := charsToDigit(time[length-2 : length]) + month := charsToDigit(time[length-4 : length-2]) if month != 2 { days, ok := monthDays[month] @@ -77,7 +77,11 @@ func validateTime(time []byte) bool { return date <= days } - if year%4 == 0 { + year := charsToDigit(time[:length-4]) + + leapYear := year%4 == 0 && year%100 != 0 || year%400 == 0 + + if leapYear { return date <= 29 } return date <= 28 @@ -124,20 +128,26 @@ func getCoOrdinationDay(day []byte) []byte { d -= 60 if d < 10 { - return []byte{'0', d + '0'} + return []byte{'0', byte(d) + '0'} } return []byte{ - d/10 + '0', - d%10 + '0', + byte(d)/10 + '0', + byte(d)%10 + '0', } } -// charsToDigit converts char bytes to the digit +// charsToDigit converts char bytes to a digit // example: ['1', '1'] => 11 -func charsToDigit(chars []byte) byte { - if len(chars) == 1 { - return chars[0] - '0' +func charsToDigit(chars []byte) int { + l := len(chars) + r := 0 + for i, c := range chars { + p := int((c - '0')) + for j := 0; j < l-i-1; j++ { + p *= 10 + } + r += p } - return (chars[0]-'0')*10 + chars[1] - '0' + return r } diff --git a/personnummer_test.go b/personnummer_test.go index 772c2ef..88d5abf 100644 --- a/personnummer_test.go +++ b/personnummer_test.go @@ -46,6 +46,13 @@ func TestPersonnummerWithWrongPersonnummerOrTypes(t *testing.T) { assert.False(t, Valid("9909193776")) } +func TestLeapYear(t *testing.T) { + assert.True(t, Valid("20000229-0005")) // Divisible by 400 + assert.False(t, Valid("19000229-0005")) // Divisible by 100 + assert.True(t, Valid("20080229-0007")) // Divisible by 4 + assert.False(t, Valid("20090229-0006")) // Not divisible by +} + func TestCoOrdinationNumbers(t *testing.T) { assert.True(t, Valid("701063-2391")) assert.True(t, Valid("640883-3231"))