diff --git a/README.md b/README.md index 4057403..7c6bef8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Swedish Personnummer Generator -A simple command-line interface (CLI) application written in Go that generates valid [Swedish "Personnummer" (personal identification numbers)](https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)). +A simple command-line interface (CLI) application written in Go that generates or validates [Swedish "Personnummer" (personal identification numbers)](https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)). ## Install @@ -13,24 +13,35 @@ A simple command-line interface (CLI) application written in Go that generates v go install github.com/dominikwinter/pno@latest ``` ## Usage +generate a personnummer: ```bash pno gen -h Usage of pno gen: -c string - Country code (default random) + Country code (default random) -d string - Date (format: yyyymmdd) (default random) + Date (format: yyyymmdd) (default random) -f string - Output forma (default 3): - 1. yymmddccgn - 2. yymmdd-ccgn - 3. yyyymmddccgn - 4. yyyymmdd-ccgn + Output forma (default 3): + 1. yymmddccgn + 2. yymmdd-ccgn + 3. yyyymmddccgn + 4. yyyymmdd-ccgn -g string - Gender (m/f) (default random) - -h Help - -v Verbose + Gender (m/f) (default random) + -h Help + -v Verbose +``` + +validate a personnummer: +```bash +pno val -h + +Usage of pno val : + -e Return exit code 1 if not valid + -h Help + -v Verbose ``` ## Development diff --git a/cmd/gen.go b/cmd/gen.go index d528fe9..3b3ac8d 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -7,7 +7,7 @@ import ( "github.com/dominikwinter/pno/internal/gen" ) -func Run(args []string) { +func Gen(args []string) { fs := flag.NewFlagSet("pno gen", flag.ExitOnError) helpFlag := fs.Bool("h", false, "Help") diff --git a/cmd/val.go b/cmd/val.go new file mode 100644 index 0000000..bb7caf2 --- /dev/null +++ b/cmd/val.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "flag" + "fmt" + "os" + + "github.com/dominikwinter/pno/internal/val" +) + +func Val(args []string) { + fs := flag.NewFlagSet("pno val ", flag.ExitOnError) + + helpFlag := fs.Bool("h", false, "Help") + verboseFlag := fs.Bool("v", false, "Verbose") + exitFlag := fs.Bool("e", false, "Return exit code 1 if not valid") + + fs.Parse(args) + + if *helpFlag || len(args) == 0 { + fs.Usage() + return + } + + pno := args[len(args)-1] + valid, _, err := val.ValidatePno(pno) + + if *verboseFlag { + if err != nil { + panic(err) + } + } + + if *exitFlag { + if valid { + os.Exit(0) + } else { + os.Exit(1) + } + return + } else { + if valid { + fmt.Println("valid") + } else { + fmt.Println("invalid") + } + return + } +} diff --git a/internal/val/validate.go b/internal/val/validate.go new file mode 100644 index 0000000..b985104 --- /dev/null +++ b/internal/val/validate.go @@ -0,0 +1,67 @@ +package val + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/dominikwinter/pno/internal/gen" +) + +var re = regexp.MustCompile(`(\d{2,4})(\d{2})(\d{2})-?(\d{2})(\d)(\d)`) + +func ValidatePno(pno string) (bool, string, error) { + results := re.FindAllStringSubmatch(pno, -1) + + if len(results) != 1 { + return false, "", errors.New("invalid format") + } + + matches := results[0] + + if len(matches) != 7 { + return false, "", errors.New("invalid format") + } + + year, err := fillYear(matches[1]) + + if err != nil { + return false, "", err + } + + date := year + matches[2] + matches[3] + country := matches[4] + gender := matches[5] + checksum := matches[6] + + genChecksum, err := gen.GetChecksum(date, country, gender) + + if err != nil { + return false, "", err + } + + return genChecksum == checksum, genChecksum, nil +} + +func fillYear(year string) (string, error) { + if len(year) == 2 { + currentYear := time.Now().Year() + year, err := strconv.Atoi(year) + + if err != nil { + return "", fmt.Errorf("invalid year input: %w", err) + } + + century := currentYear / 100 + + if year < (currentYear % 100) { + century++ + } + + return strconv.Itoa(century*100 + year), nil + } + + return year, nil +} diff --git a/internal/val/validate_test.go b/internal/val/validate_test.go new file mode 100644 index 0000000..099db97 --- /dev/null +++ b/internal/val/validate_test.go @@ -0,0 +1,87 @@ +package val + +import ( + "testing" +) + +func Test_ValidatePno(t *testing.T) { + t.Run("valid", func(t *testing.T) { + tests := [...]string{ + "4112098357", + "3311305498", + "7112050112", + "7412188927", + "3812145997", + "6701203199", + "8004113877", + "9806275906", + "2802107041", + "0007060833", + "991227-0171", + "720813-5637", + "370123-4878", + "350717-7297", + "871010-5225", + "651214-3329", + "041008-1400", + "750930-7117", + "821010-9701", + "200303178578", + "193902179021", + "198801266217", + "197511269453", + "200505206169", + "199304288872", + "195012150594", + "200003170933", + "196908054858", + "199702051559", + "200010201317", + "196106122234", + "19410308-0778", + "19341028-8843", + "19840928-9744", + "19340815-3322", + "19921220-7667", + "20000104-6416", + "19721218-8754", + "20020226-3695", + } + + for _, pno := range tests { + valid, checksum, err := ValidatePno(pno) + if err != nil { + t.Error(err) + } + + if !valid { + t.Errorf("'%s' should be a valid PNO. checksum is '%s'", pno, checksum) + } + } + }) + + t.Run("invalid", func(t *testing.T) { + tests := [...]string{ + "198003257629", + "19800325762", + "1980032576", + "198003257", + "19800325", + "1980032", + "198003", + "19800", + "1980", + "198", + "19", + "1", + } + + for _, pno := range tests { + valid, checksum, _ := ValidatePno(pno) + + if valid { + t.Errorf("'%s' should be a invalid PNO. checksum is '%s'", pno, checksum) + } + } + }) +} diff --git a/main.go b/main.go index fa35831..fb8997e 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,11 @@ import ( ) func main() { - const usage = `Usage of pno []: + const usage = `Usage of pno [] commands: gen Generate a personal number + val Validate a personal number args: -h Help` @@ -26,7 +27,11 @@ func main() { switch command { case "gen": - cmd.Run(args) + cmd.Gen(args) + + case "val": + cmd.Val(args) + default: fmt.Fprintf(os.Stderr, "Unrecognized command %s.\n\n%s\n", command, usage) os.Exit(1)