├── .gitignore ├── go.mod ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── namestring.go ├── nameparts.go └── nameparts_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | _workspace 2 | .idea 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/polera/gonameparts 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | husobee - https://github.com/husobee 2 | idubinskiy - https://github.com/idubinskiy 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Polera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gonameparts 2 | gonameparts splits a human name into individual parts. This is useful when dealing with external data sources that provide names as a single value, but you need to store the discrete parts in a database for example. 3 | 4 | [![GoDoc](https://godoc.org/github.com/polera/gonameparts?status.svg)](https://godoc.org/github.com/polera/gonameparts) 5 | 6 | Author 7 | == 8 | James Polera 9 | 10 | Dependencies 11 | == 12 | No external dependencies. Uses Go's standard packages 13 | 14 | Example 15 | == 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | 24 | "github.com/polera/gonameparts" 25 | ) 26 | 27 | func main() { 28 | 29 | // Parsing a name and printing its parts 30 | nameString := gonameparts.Parse("Thurston Howell III") 31 | fmt.Println("FirstName:", nameString.FirstName) 32 | fmt.Println("LastName:", nameString.LastName) 33 | fmt.Println("Generation:", nameString.Generation) 34 | // Output: 35 | // FirstName: Thurston 36 | // LastName: Howell 37 | // Generation: III 38 | 39 | // Parse a name with multiple "also known as" aliases, output JSON 40 | multipleAKA := gonameparts.Parse("Tony Stark a/k/a Ironman a/k/a Stark, Anthony a/k/a Anthony Edward \"Tony\" Stark") 41 | jsonParts, _ := json.Marshal(multipleAKA) 42 | fmt.Printf("%v\n", string(jsonParts)) 43 | /* Output: 44 | { 45 | "aliases": [ 46 | { 47 | "aliases": null, 48 | "first_name": "Ironman", 49 | "full_name": "Ironman", 50 | "generation": "", 51 | "last_name": "", 52 | "middle_name": "", 53 | "nickname": "", 54 | "provided_name": " Ironman ", 55 | "salutation": "", 56 | "suffix": "" 57 | }, 58 | { 59 | "aliases": null, 60 | "first_name": "Anthony", 61 | "full_name": "Anthony Stark", 62 | "generation": "", 63 | "last_name": "Stark", 64 | "middle_name": "", 65 | "nickname": "", 66 | "provided_name": " Stark, Anthony ", 67 | "salutation": "", 68 | "suffix": "" 69 | }, 70 | { 71 | "aliases": null, 72 | "first_name": "Anthony", 73 | "full_name": "Anthony Edward Stark", 74 | "generation": "", 75 | "last_name": "Stark", 76 | "middle_name": "Edward", 77 | "nickname": "\"Tony\"", 78 | "provided_name": " Anthony Edward \"Tony\" Stark", 79 | "salutation": "", 80 | "suffix": "" 81 | } 82 | ], 83 | "first_name": "Tony", 84 | "full_name": "Tony Stark", 85 | "generation": "", 86 | "last_name": "Stark", 87 | "middle_name": "", 88 | "nickname": "", 89 | "provided_name": "Tony Stark a/k/a Ironman a/k/a Stark, Anthony a/k/a Anthony Edward \"Tony\" Stark", 90 | "salutation": "", 91 | "suffix": "" 92 | }*/ 93 | 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /namestring.go: -------------------------------------------------------------------------------- 1 | package gonameparts 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | type nameString struct { 9 | FullName string 10 | SplitName []string 11 | Nickname string 12 | Aliases []string 13 | } 14 | 15 | func (n *nameString) cleaned() []string { 16 | unwanted := []string{",", "."} 17 | cleaned := []string{} 18 | for _, x := range n.split() { 19 | for _, y := range unwanted { 20 | x = strings.Replace(x, y, "", -1) 21 | } 22 | cleaned = append(cleaned, strings.Trim(x, " ")) 23 | } 24 | return cleaned 25 | } 26 | 27 | func (n *nameString) searchParts(parts []string) int { 28 | for i, x := range n.cleaned() { 29 | for _, y := range parts { 30 | if strings.ToUpper(x) == strings.ToUpper(y) { 31 | return i 32 | } 33 | } 34 | } 35 | return -1 36 | } 37 | 38 | func (n *nameString) looksCorporate() bool { 39 | return n.searchParts(corpEntity) > -1 40 | } 41 | 42 | func (n *nameString) hasComma() bool { 43 | for _, x := range n.split() { 44 | if strings.ContainsAny(x, ",") { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | func (n *nameString) slotNickname() { 52 | var nickNameBoundaries []int 53 | 54 | for index, x := range n.split() { 55 | if string(x[0]) == "'" || x[0] == '"' { 56 | nickNameBoundaries = append(nickNameBoundaries, index) 57 | } 58 | if string(x[len(x)-1]) == "'" || x[len(x)-1] == '"' { 59 | nickNameBoundaries = append(nickNameBoundaries, index) 60 | } 61 | } 62 | 63 | if len(nickNameBoundaries) > 0 && len(nickNameBoundaries)%2 == 0 { 64 | nickStart := nickNameBoundaries[0] 65 | nickEnd := nickNameBoundaries[1] 66 | 67 | nick := n.SplitName[:nickStart] 68 | postNick := n.SplitName[nickEnd+1:] 69 | 70 | n.Nickname = strings.Join(n.SplitName[nickStart:nickEnd+1], " ") 71 | nick = append(nick, postNick...) 72 | n.FullName = strings.Join(nick, " ") 73 | } 74 | } 75 | 76 | func (n *nameString) fixMisplacedApostrophe() { 77 | var endsWithApostrophe []int 78 | 79 | for index, x := range n.split() { 80 | if string(x[len(x)-1]) == "'" { 81 | endsWithApostrophe = append(endsWithApostrophe, index) 82 | } 83 | } 84 | 85 | if len(endsWithApostrophe) > 0 { 86 | for _, y := range endsWithApostrophe { 87 | if n.SplitName[y] == n.SplitName[len(n.SplitName)-1] { 88 | tmpName := n.SplitName[:y] 89 | tmpName = append(tmpName, strings.Trim(n.SplitName[y], "'")) 90 | n.FullName = strings.Join(tmpName, " ") 91 | } else { 92 | misplacedStart := y 93 | // Build a new name part composed of the misplaced apostrophe 94 | // plus what it should be attached to (i.e. O' Hurley becomes O'Hurley) 95 | fixedName := []string{n.SplitName[misplacedStart]} 96 | fixedName = append(fixedName, n.SplitName[misplacedStart+1]) 97 | fixedPlacement := strings.Join(fixedName, "") 98 | 99 | // Rebuild our FullName with our fixedPlacement 100 | tmpName := n.SplitName[:misplacedStart] 101 | tmpName = append(tmpName, fixedPlacement) 102 | partsAfterMisplacedStart := n.SplitName[misplacedStart+2:] 103 | tmpName = append(tmpName, partsAfterMisplacedStart...) 104 | n.FullName = strings.Join(tmpName, " ") 105 | } 106 | } 107 | } 108 | } 109 | 110 | func (n *nameString) hasAliases() (bool, string) { 111 | upperName := strings.ToUpper(n.FullName) 112 | for _, x := range nonName { 113 | if strings.Contains(upperName, x) && !strings.HasSuffix(upperName, x) { 114 | return true, x 115 | } 116 | } 117 | return false, "" 118 | } 119 | 120 | func (n *nameString) find(part string) int { 121 | switch part { 122 | case "salutation": 123 | return n.searchParts(salutations) 124 | case "generation": 125 | return n.searchParts(generations) 126 | case "suffix": 127 | return n.searchParts(suffixes) 128 | case "lnprefix": 129 | return n.searchParts(lnPrefixes) 130 | case "nonname": 131 | return n.searchParts(nonName) 132 | case "supplemental": 133 | return n.searchParts(supplementalInfo) 134 | default: 135 | 136 | } 137 | return -1 138 | } 139 | 140 | func (n *nameString) split() []string { 141 | 142 | n.SplitName = strings.Fields(n.FullName) 143 | return n.SplitName 144 | } 145 | 146 | func (n *nameString) normalize() []string { 147 | // Handle any aliases in our nameString 148 | hasAlias, aliasSep := n.hasAliases() 149 | 150 | if hasAlias { 151 | n.splitAliases(aliasSep) 152 | } 153 | 154 | // Strip Supplemental info 155 | supplementalIndex := n.find("supplemental") 156 | if supplementalIndex > -1 { 157 | n.FullName = strings.Join(n.SplitName[:supplementalIndex], " ") 158 | } 159 | 160 | // Handle quoted Nicknames 161 | n.slotNickname() 162 | 163 | // Handle misplaced apostrophes 164 | n.fixMisplacedApostrophe() 165 | 166 | // Swap Lastname, Firstname to Firstname Lastname 167 | if n.hasComma() { 168 | commaSplit := strings.SplitN(n.FullName, ",", 2) 169 | sort.StringSlice(commaSplit).Swap(1, 0) 170 | n.FullName = strings.Join(commaSplit, " ") 171 | } 172 | 173 | return n.cleaned() 174 | } 175 | 176 | func (n *nameString) splitAliases(aliasSep string) { 177 | splitNames := n.split() 178 | 179 | for index, part := range splitNames { 180 | if strings.ToUpper(part) == aliasSep { 181 | splitNames[index] = "*|*" 182 | } 183 | } 184 | 185 | names := strings.Split(strings.Join(splitNames, " "), "*|*") 186 | n.FullName = names[0] 187 | n.Aliases = names[1:] 188 | } 189 | 190 | func (n *nameString) findNotSlotted(slotted []int) []int { 191 | var notSlotted []int 192 | 193 | for i := range n.SplitName { 194 | found := false 195 | for _, j := range slotted { 196 | if i == j { 197 | found = true 198 | break 199 | } 200 | } 201 | if !found { 202 | notSlotted = append(notSlotted, i) 203 | } 204 | } 205 | return notSlotted 206 | } 207 | -------------------------------------------------------------------------------- /nameparts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gonameparts splits a human name into individual parts. This is useful 3 | when dealing with external data sources that provide names as a single value, but 4 | you need to store the discrete parts in a database for example. 5 | */ 6 | package gonameparts 7 | 8 | import ( 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | /* 14 | Identifiable name parts 15 | */ 16 | var ( 17 | salutations = []string{"MR", "MS", "MRS", "DR", "MISS", "DOCTOR", "CORP", "SGT", "PVT", "JUDGE", "CAPT", "COL", "MAJ", "LT", "LIEUTENANT", "PRM", "PATROLMAN", "HON", "OFFICER", "REV", "PRES", "PRESIDENT", "GOV", "GOVERNOR", "VICE PRESIDENT", "VP", "MAYOR", "SIR", "MADAM", "HONORABLE"} 18 | generations = []string{"JR", "SR", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "1ST", "2ND", "3RD", "4TH", "5TH", "6TH", "7TH", "8TH", "9TH", "10TH", "FIRST", "SECOND", "THIRD", "FOURTH", "FIFTH", "SIXTH", "SEVENTH", "EIGHTH", "NINTH", "TENTH"} 19 | suffixes = []string{"ESQ", "PHD", "MD"} 20 | lnPrefixes = []string{"DE", "DA", "DI", "LA", "DU", "DEL", "DEI", "VDA", "DELLO", "DELLA", "DEGLI", "DELLE", "VAN", "VON", "DER", "DEN", "HEER", "TEN", "TER", "VANDE", "VANDEN", "VANDER", "VOOR", "VER", "AAN", "MC", "BEN", "SAN", "SAINZ", "BIN", "LI", "LE", "DES", "AM", "AUS'M", "VOM", "ZUM", "ZUR", "TEN", "IBN"} 21 | nonName = []string{"A.K.A", "AKA", "A/K/A", "F.K.A", "FKA", "F/K/A", "N/K/A"} 22 | corpEntity = []string{"NA", "CORP", "CO", "INC", "ASSOCIATES", "SERVICE", "LLC", "LLP", "PARTNERS", "R/A", "C/O", "COUNTY", "STATE", "BANK", "GROUP", "MUTUAL", "FARGO"} 23 | supplementalInfo = []string{"WIFE OF", "HUSBAND OF", "SON OF", "DAUGHTER OF", "DECEASED", "FICTITIOUS"} 24 | ) 25 | 26 | /* 27 | NameParts represents the slotted components of a given name 28 | */ 29 | type NameParts struct { 30 | ProvidedName string `json:"provided_name"` 31 | FullName string `json:"full_name"` 32 | Salutation string `json:"salutation"` 33 | FirstName string `json:"first_name"` 34 | MiddleName string `json:"middle_name"` 35 | LastName string `json:"last_name"` 36 | Generation string `json:"generation"` 37 | Suffix string `json:"suffix"` 38 | Nickname string `json:"nickname"` 39 | Aliases []NameParts `json:"aliases"` 40 | } 41 | 42 | func (p *NameParts) slot(part string, value string) { 43 | switch part { 44 | case "salutation": 45 | p.Salutation = value 46 | case "generation": 47 | p.Generation = value 48 | case "suffix": 49 | p.Suffix = value 50 | case "middle": 51 | p.MiddleName = value 52 | case "last": 53 | p.LastName = value 54 | case "first": 55 | p.FirstName = value 56 | default: 57 | 58 | } 59 | 60 | } 61 | 62 | func (p *NameParts) buildFullName() { 63 | var fullNameParts []string 64 | 65 | if len(p.Salutation) > 0 { 66 | fullNameParts = append(fullNameParts, p.Salutation) 67 | } 68 | 69 | if len(p.FirstName) > 0 { 70 | fullNameParts = append(fullNameParts, p.FirstName) 71 | } 72 | 73 | if len(p.MiddleName) > 0 { 74 | fullNameParts = append(fullNameParts, p.MiddleName) 75 | } 76 | 77 | if len(p.LastName) > 0 { 78 | fullNameParts = append(fullNameParts, p.LastName) 79 | } 80 | 81 | if len(p.Generation) > 0 { 82 | fullNameParts = append(fullNameParts, p.Generation) 83 | } 84 | 85 | if len(p.Suffix) > 0 { 86 | fullNameParts = append(fullNameParts, p.Suffix) 87 | } 88 | 89 | p.FullName = strings.Join(fullNameParts, " ") 90 | 91 | } 92 | 93 | /* 94 | Parse takes a string name as a parameter and returns a populated NameParts object 95 | */ 96 | func Parse(name string) NameParts { 97 | n := nameString{FullName: name} 98 | n.normalize() 99 | 100 | p := NameParts{ProvidedName: name, Nickname: n.Nickname} 101 | 102 | // If we're dealing with a business name, just return it back 103 | if n.looksCorporate() { 104 | return p 105 | } 106 | 107 | parts := []string{"generation", "suffix", "lnprefix", "supplemental"} 108 | partMap := make(map[string]int) 109 | var slotted []int 110 | 111 | // Slot and index parts 112 | for _, part := range parts { 113 | partIndex := n.find(part) 114 | partMap[part] = partIndex 115 | if partIndex > -1 { 116 | p.slot(part, n.SplitName[partIndex]) 117 | slotted = append(slotted, partIndex) 118 | } 119 | } 120 | 121 | // Find salutation, but make sure it's first; otherwise it may be a false positive 122 | if salIndex := n.find("salutation"); salIndex == 0 { 123 | partMap["salutation"] = salIndex 124 | p.slot("salutation", n.SplitName[salIndex]) 125 | slotted = append(slotted, salIndex) 126 | } else { 127 | partMap["salutation"] = -1 128 | } 129 | 130 | // Find nonname, but make sure it's not last; otherwise it may be a false positive 131 | if nnIndex := n.find("nonname"); nnIndex > -1 && nnIndex < len(n.SplitName)-1 { 132 | partMap["nonname"] = nnIndex 133 | p.slot("nonname", n.SplitName[nnIndex]) 134 | slotted = append(slotted, nnIndex) 135 | } else { 136 | partMap["nonname"] = -1 137 | } 138 | 139 | // Slot FirstName 140 | 141 | firstPos := partMap["salutation"] + 1 142 | if firstPos == len(n.SplitName) { 143 | p.buildFullName() 144 | return p 145 | } 146 | partMap["first"] = firstPos 147 | p.slot("first", n.SplitName[partMap["first"]]) 148 | slotted = append(slotted, firstPos) 149 | 150 | // Slot prefixed LastName 151 | if partMap["lnprefix"] > -1 { 152 | lnEnd := len(n.SplitName) 153 | if partMap["generation"] > -1 { 154 | lnEnd = partMap["generation"] 155 | } 156 | if partMap["suffix"] > -1 && partMap["generation"] < lnEnd { 157 | lnEnd = partMap["suffix"] 158 | } 159 | // Need to validate the slice parameters make sense 160 | // Example Name: "I am the Popsicle" 161 | // This example causes a hit on the generation at position 0, 162 | // which in turn causes lnEnd to be set to 0, but the lnprefix 163 | // is greater than 0, causing a slice out of bounds panic 164 | if lnEnd > partMap["lnprefix"] { 165 | p.slot("last", strings.Join(n.SplitName[partMap["lnprefix"]:lnEnd], " ")) 166 | } 167 | 168 | // Keep track of what we've slotted 169 | for i := partMap["lnprefix"]; i <= lnEnd; i++ { 170 | slotted = append(slotted, i) 171 | } 172 | } 173 | 174 | // Slot the rest 175 | notSlotted := n.findNotSlotted(slotted) 176 | 177 | if len(notSlotted) > 1 { 178 | lnPrefix := partMap["lnprefix"] 179 | var multiMiddle []string 180 | if lnPrefix > -1 { 181 | for p := range notSlotted { 182 | multiMiddle = append(multiMiddle, n.SplitName[p]) 183 | } 184 | p.slot("middle", strings.Join(multiMiddle, " ")) 185 | 186 | } else { 187 | sort.Sort(sort.IntSlice(notSlotted)) 188 | maxNotSlottedIndex := notSlotted[len(notSlotted)-1] 189 | p.slot("last", n.SplitName[maxNotSlottedIndex]) 190 | 191 | for _, p := range notSlotted { 192 | if p != maxNotSlottedIndex { 193 | multiMiddle = append(multiMiddle, n.SplitName[p]) 194 | } 195 | } 196 | p.slot("middle", strings.Join(multiMiddle, " ")) 197 | } 198 | } 199 | 200 | if len(notSlotted) == 1 { 201 | if partMap["lnprefix"] > -1 { 202 | p.slot("middle", n.SplitName[notSlotted[0]]) 203 | } else { 204 | p.slot("last", n.SplitName[notSlotted[0]]) 205 | } 206 | } 207 | 208 | // Process aliases 209 | for _, alias := range n.Aliases { 210 | p.Aliases = append(p.Aliases, Parse(alias)) 211 | } 212 | 213 | // Prepare FullName 214 | p.buildFullName() 215 | 216 | return p 217 | } 218 | -------------------------------------------------------------------------------- /nameparts_test.go: -------------------------------------------------------------------------------- 1 | package gonameparts 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestLooksCorporate(t *testing.T) { 9 | t.Parallel() 10 | n := nameString{FullName: "Sprockets Inc"} 11 | 12 | res := n.looksCorporate() 13 | 14 | if res != true { 15 | t.Errorf("Expected true. Actual %v", res) 16 | } 17 | 18 | } 19 | 20 | func TestSearchParts(t *testing.T) { 21 | t.Parallel() 22 | n := nameString{FullName: "Mr. James Polera"} 23 | 24 | res := n.searchParts(salutations) 25 | 26 | if res != 0 { 27 | t.Errorf("Expected true. Actual %v", res) 28 | } 29 | 30 | } 31 | 32 | func TestClean(t *testing.T) { 33 | t.Parallel() 34 | n := nameString{FullName: "Mr. James Polera"} 35 | 36 | res := n.cleaned() 37 | 38 | if res[0] != "Mr" { 39 | t.Errorf("Expected 'Mr'. Actual %v", res[0]) 40 | } 41 | 42 | } 43 | 44 | func TestLocateSalutation(t *testing.T) { 45 | t.Parallel() 46 | n := nameString{FullName: "Mr. James Polera"} 47 | 48 | res := n.find("salutation") 49 | 50 | if res != 0 { 51 | t.Errorf("Expected 0. Actual %v", res) 52 | } 53 | } 54 | 55 | func TestHasComma(t *testing.T) { 56 | t.Parallel() 57 | n := nameString{FullName: "Polera, James"} 58 | res := n.hasComma() 59 | 60 | if res != true { 61 | t.Errorf("Expected true. Actual %v", res) 62 | } 63 | 64 | } 65 | 66 | func TestNormalize(t *testing.T) { 67 | t.Parallel() 68 | n := nameString{FullName: "Polera, James"} 69 | res := n.normalize() 70 | 71 | if res[0] != "James" { 72 | t.Errorf("Expected James. Actual %v", res[0]) 73 | } 74 | 75 | if res[1] != "Polera" { 76 | t.Errorf("Expected Polera. Actual %v", res[1]) 77 | } 78 | 79 | } 80 | 81 | func TestParseAllFields(t *testing.T) { 82 | t.Parallel() 83 | res := Parse("Mr. James J. Polera Jr. Esq.") 84 | 85 | if res.Salutation != "Mr." { 86 | t.Errorf("Expected 'Mr.'. Actual %v", res.Salutation) 87 | } 88 | 89 | if res.FirstName != "James" { 90 | t.Errorf("Expected 'James'. Actual %v", res.FirstName) 91 | } 92 | 93 | if res.MiddleName != "J." { 94 | t.Errorf("Expected 'J.'. Actual %v", res.MiddleName) 95 | } 96 | 97 | if res.LastName != "Polera" { 98 | t.Errorf("Expected 'Polera'. Actual %v", res.LastName) 99 | } 100 | 101 | if res.Generation != "Jr." { 102 | t.Errorf("Expected 'Jr.'. Actual %v", res.Generation) 103 | } 104 | 105 | if res.Suffix != "Esq." { 106 | t.Errorf("Expected 'Esq.'. Actual %v", res.Suffix) 107 | } 108 | } 109 | 110 | func TestParseOnlySalutation(t *testing.T) { 111 | t.Parallel() 112 | 113 | res := Parse("Mr.") 114 | if res.FirstName != "" { 115 | t.Errorf("Expected ''. Actual %v", res.FirstName) 116 | } 117 | 118 | if res.LastName != "" { 119 | t.Errorf("Expected ''. Actual %v", res.LastName) 120 | } 121 | } 122 | 123 | func TestParseFirstLast(t *testing.T) { 124 | t.Parallel() 125 | 126 | res := Parse("James Polera") 127 | if res.FirstName != "James" { 128 | t.Errorf("Expected 'James'. Actual %v", res.FirstName) 129 | } 130 | 131 | if res.LastName != "Polera" { 132 | t.Errorf("Expected 'Polera'. Actual %v", res.LastName) 133 | } 134 | } 135 | 136 | func TestLastNamePrefix(t *testing.T) { 137 | t.Parallel() 138 | 139 | res := Parse("Otto von Bismark") 140 | 141 | if res.FirstName != "Otto" { 142 | t.Errorf("Expected 'Otto'. Actual %v", res.FirstName) 143 | } 144 | 145 | if res.LastName != "von Bismark" { 146 | t.Errorf("Expected 'von Bismark'. Actual %v", res.LastName) 147 | } 148 | 149 | } 150 | 151 | func TestAliases(t *testing.T) { 152 | t.Parallel() 153 | 154 | res := Parse("James Polera a/k/a Batman") 155 | 156 | if res.Aliases[0].FirstName != "Batman" { 157 | t.Errorf("Expected 'Batman'. Actual: %v", res.Aliases[0].FirstName) 158 | } 159 | 160 | } 161 | 162 | func TestNickname(t *testing.T) { 163 | t.Parallel() 164 | 165 | res := Parse("Philip Francis 'The Scooter' Rizzuto") 166 | 167 | if res.Nickname != "'The Scooter'" { 168 | t.Errorf("Expected 'The Scooter'. Actual: %v", res.Nickname) 169 | } 170 | } 171 | 172 | func TestStripSupplemental(t *testing.T) { 173 | t.Parallel() 174 | 175 | res := Parse("Philip Francis 'The Scooter' Rizzuto, deceased") 176 | 177 | if res.FirstName != "Philip" { 178 | t.Errorf("Expected 'Philip'. Actual: %v", res.FirstName) 179 | } 180 | 181 | if res.MiddleName != "Francis" { 182 | t.Errorf("Expected 'Francis'. Actual: %v", res.MiddleName) 183 | } 184 | 185 | if res.Nickname != "'The Scooter'" { 186 | t.Errorf("Expected 'The Scooter'. Actual: %v", res.Nickname) 187 | } 188 | 189 | if res.LastName != "Rizzuto" { 190 | t.Errorf("Expected 'Rizzuto'. Actual: %v", res.LastName) 191 | } 192 | } 193 | 194 | func TestLongPrefixedLastName(t *testing.T) { 195 | t.Parallel() 196 | 197 | res := Parse("Saleh ibn Tariq ibn Khalid al-Fulan") 198 | 199 | if res.FirstName != "Saleh" { 200 | t.Errorf("Expected 'Saleh'. Actual: %v", res.FirstName) 201 | } 202 | 203 | if res.LastName != "ibn Tariq ibn Khalid al-Fulan" { 204 | t.Errorf("Expected 'ibn Tariq ibn Khalid al-Fulan'. Actual: %v", res.LastName) 205 | 206 | } 207 | } 208 | 209 | func TestMisplacedApostrophe(t *testing.T) { 210 | t.Parallel() 211 | 212 | res := Parse("John O' Hurley") 213 | 214 | if res.FirstName != "John" { 215 | t.Errorf("Expected 'John'. Actual: %v", res.FirstName) 216 | } 217 | 218 | if res.LastName != "O'Hurley" { 219 | t.Errorf("Expected 'O'Hurley'. Actual: %v", res.LastName) 220 | } 221 | 222 | } 223 | 224 | func TestMultipleAKA(t *testing.T) { 225 | t.Parallel() 226 | 227 | res := Parse("Tony Stark a/k/a Ironman a/k/a Stark, Anthony a/k/a Anthony Edward \"Tony\" Stark") 228 | 229 | if len(res.Aliases) != 3 { 230 | t.Errorf("Expected 3 aliases. Actual: %v", len(res.Aliases)) 231 | } 232 | 233 | if res.FirstName != "Tony" { 234 | t.Errorf("Expected 'Tony'. Actual: %v", res.FirstName) 235 | } 236 | 237 | if res.LastName != "Stark" { 238 | t.Errorf("Expected 'Stark'. Actual: %v", res.LastName) 239 | } 240 | 241 | } 242 | 243 | func TestBuildFullName(t *testing.T) { 244 | res := Parse("President George Herbert Walker Bush") 245 | 246 | if res.FullName != "President George Herbert Walker Bush" { 247 | 248 | t.Errorf("Expected 'President George Herbert Walker Bush'. Actual: %v", res.FullName) 249 | } 250 | 251 | } 252 | 253 | func TestDottedAka(t *testing.T) { 254 | res := Parse("James Polera a.k.a James K. Polera") 255 | if len(res.Aliases) != 1 { 256 | t.Errorf("Expected 1 alias. Actual: %v", len(res.Aliases)) 257 | } 258 | } 259 | 260 | func TestUnicodeCharsInName(t *testing.T) { 261 | res := Parse("König Ludwig") 262 | 263 | if res.FirstName != "König" { 264 | t.Errorf("Expected 'König'. Actual: %v", res.FirstName) 265 | } 266 | } 267 | 268 | func TestTabsInName(t *testing.T) { 269 | res := Parse("Dr. James\tPolera\tEsq.") 270 | 271 | if res.Salutation != "Dr." { 272 | t.Errorf("Expected 'Dr.'. Actual: %v", res.Salutation) 273 | } 274 | 275 | if res.FirstName != "James" { 276 | t.Errorf("Expected 'James'. Actual: %v", res.FirstName) 277 | } 278 | 279 | if res.LastName != "Polera" { 280 | t.Errorf("Expected 'Polera'. Actual: %v", res.LastName) 281 | } 282 | 283 | if res.Suffix != "Esq." { 284 | t.Errorf("Expected 'Esq.'. Actual: %v", res.Suffix) 285 | } 286 | } 287 | 288 | func TestObviouslyBadName(t *testing.T) { 289 | // make sure we don't panic on a clearly bad name 290 | defer func() { 291 | if r := recover(); r != nil { 292 | // panic happened, fail the test 293 | t.Errorf("Panic happened, where it shouldn't have") 294 | } 295 | }() 296 | Parse("I am a Popsicle") 297 | } 298 | 299 | func TestLastNameSalutation(t *testing.T) { 300 | // make sure we don't panic if the last name looks like a salutation 301 | defer func() { 302 | if r := recover(); r != nil { 303 | // panic happened, fail the test 304 | t.Errorf("Panic happened, where it shouldn't have") 305 | } 306 | }() 307 | res := Parse("Alan Hon") 308 | 309 | if res.FirstName != "Alan" { 310 | t.Errorf("Expected 'Alan'. Actual: %v", res.FirstName) 311 | } 312 | 313 | if res.LastName != "Hon" { 314 | t.Errorf("Expected 'Hon'. Actual: %v", res.LastName) 315 | } 316 | 317 | if res.FullName != "Alan Hon" { 318 | t.Errorf("Expected 'Alan Hon'. Actual: %v", res.FullName) 319 | } 320 | } 321 | 322 | func TestLastNameNonName(t *testing.T) { 323 | // make sure we don't panic if the last name looks like a nonname 324 | defer func() { 325 | if r := recover(); r != nil { 326 | // panic happened, fail the test 327 | t.Errorf("Panic happened, where it shouldn't have") 328 | } 329 | }() 330 | res := Parse("Jessica Aka") 331 | 332 | if res.FirstName != "Jessica" { 333 | t.Errorf("Expected 'Jessica'. Actual: %v", res.FirstName) 334 | } 335 | 336 | if res.LastName != "Aka" { 337 | t.Errorf("Expected 'Aka'. Actual: %v", res.LastName) 338 | } 339 | 340 | if res.FullName != "Jessica Aka" { 341 | t.Errorf("Expected 'Jessica Aka'. Actual: %v", res.FullName) 342 | } 343 | } 344 | 345 | func TestNameEndsWithApostrophe(t *testing.T) { 346 | // make sure we don't panic on a clearly bad name 347 | defer func() { 348 | if r := recover(); r != nil { 349 | // panic happened, fail the test 350 | t.Errorf("Panic happened, where it shouldn't have") 351 | } 352 | }() 353 | res := Parse("James Polera'") 354 | if res.FirstName != "James" { 355 | t.Errorf("Expected 'James'. Actual: %v", res.FirstName) 356 | } 357 | 358 | if res.LastName != "Polera" { 359 | t.Errorf("Expected 'Polera'. Actual: %v", res.LastName) 360 | } 361 | } 362 | 363 | func ExampleParse() { 364 | res := Parse("Thurston Howell III") 365 | fmt.Println("FirstName:", res.FirstName) 366 | fmt.Println("LastName:", res.LastName) 367 | fmt.Println("Generation:", res.Generation) 368 | 369 | // Output: 370 | // FirstName: Thurston 371 | // LastName: Howell 372 | // Generation: III 373 | 374 | } 375 | 376 | func ExampleParse_second() { 377 | 378 | res := Parse("President George Herbert Walker Bush") 379 | fmt.Println("Salutation:", res.Salutation) 380 | fmt.Println("FirstName:", res.FirstName) 381 | fmt.Println("MiddleName:", res.MiddleName) 382 | fmt.Println("LastName:", res.LastName) 383 | 384 | // Output: 385 | // Salutation: President 386 | // FirstName: George 387 | // MiddleName: Herbert Walker 388 | // LastName: Bush 389 | 390 | } 391 | --------------------------------------------------------------------------------