github.com/LanderTome/numerologyCalculator@v1.0.2/numerology/nameSearch.go (about) 1 // Copyright 2021 Robert D. Wukmir 2 // This file is subject to the terms and conditions defined in 3 // the LICENSE file, which is part of this source code package. 4 // 5 // Unless required by applicable law or agreed to in writing, 6 // software distributed under the License is distributed on an 7 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 8 // either express or implied. See the License for the specific 9 // language governing permissions and limitations under the 10 // License. 11 12 package numerology 13 14 import ( 15 "fmt" 16 "gorm.io/gorm" 17 "math/rand" 18 "sort" 19 "strings" 20 "time" 21 "unicode" 22 ) 23 24 var largestNameValueInTable = map[string]int{} 25 26 // Constants that represent the name sorting methods. 27 const ( 28 CommonSort = "common" 29 UncommonSort = "uncommon" 30 RandomSort = "random" 31 ) 32 33 type queryLookup struct { 34 MinimumSearchNumber int 35 MaximumSearchNumber int 36 TargetNumbers []int 37 ColumnName string 38 MasterNumbers []int 39 ReduceWords bool 40 } 41 42 func init() { 43 // Initialize the random module. 44 rand.Seed(time.Now().UnixNano()) 45 } 46 47 // generateLookupNums finds the values to look for in the database that will result in names with the correct 48 // numerological properties. The key is to find unreduced numbers that will reduce down to the value that we 49 // want. Since all the values in the database are stored unreduced we can then find names that will work. 50 func generateLookupNums(minSearchNumber int, maxSearchNumber int, numerologyNums []int, masterNumbers []int, reduceWords bool) []int { 51 var nums []int 52 // Split up positive and negative numbers into separate lists and make negative numbers positive for comparison. 53 positiveNums := []int{} 54 negativeNums := []int{} 55 for _, n := range numerologyNums { 56 if n >= 0 { 57 positiveNums = append(positiveNums, n) 58 } else { 59 negativeNums = append(negativeNums, -n) 60 } 61 } 62 // Iterate through possible numbers looking for matches. 63 for i := 1; minSearchNumber+i <= maxSearchNumber; i++ { 64 // i needs to be in reduced form for proper calculation. 65 reducedI := reduceNumbers(i, masterNumbers, []int{}) 66 var idx int 67 if reduceWords { 68 idx = len(reducedI) - 1 69 } else { 70 idx = 0 71 } 72 // Create the reduced value of this hypothetical name. 73 validNum := reduceNumbers(minSearchNumber+reducedI[idx], masterNumbers, []int{}) 74 // Check if the validNum satisfies our numerological criteria. 75 var noMatch bool 76 // If there are positive numbers then noMatch is only false when we specifically find a number. 77 // The alternative is that any number is acceptable as long as it isn't a negative number. 78 if len(positiveNums) > 0 { 79 noMatch = true 80 } 81 for _, v := range validNum { 82 if inIntSlice(v, negativeNums) { 83 noMatch = true 84 break 85 } 86 if inIntSlice(v, positiveNums) { 87 noMatch = false 88 break 89 } 90 } 91 if !noMatch { 92 nums = append(nums, i) 93 } 94 } 95 return nums 96 } 97 98 func addQueryLookup(query *gorm.DB, q queryLookup) { 99 if len(q.TargetNumbers) == 0 { 100 return 101 } 102 lookupNums := generateLookupNums(q.MinimumSearchNumber, q.MaximumSearchNumber, q.TargetNumbers, q.MasterNumbers, q.ReduceWords) 103 if len(lookupNums) > 0 { 104 query = query.Where(q.ColumnName+" IN ?", lookupNums) 105 } else { 106 query = query.Where("FALSE") 107 } 108 return 109 } 110 111 func addKarmicDebtLookup(query *gorm.DB, q queryLookup) { 112 if len(q.TargetNumbers) == 0 { 113 return 114 } 115 lookupNums := generateLookupNums(q.MinimumSearchNumber, q.MaximumSearchNumber, q.TargetNumbers, q.MasterNumbers, q.ReduceWords) 116 if len(lookupNums) > 0 { 117 query = query.Where(q.ColumnName+" NOT IN ?", lookupNums) 118 } 119 return 120 } 121 122 func addQueryGender(query *gorm.DB, gender rune) { 123 // Add gender parameter if specified. Otherwise include all genders. 124 if unicode.ToUpper(gender) == 'M' || unicode.ToUpper(gender) == 'F' { 125 query = query.Where("gender = ?", string(unicode.ToUpper(gender))) 126 } 127 return 128 } 129 130 // Order the results based on the selected option 131 func addQuerySort(query *gorm.DB, sort string, offset int, seed int64) { 132 switch strings.ToLower(sort) { 133 case UncommonSort: 134 // Todo: Do not hardcode uncommon skip value 135 // Skip down a ways so the names are less common. 136 skip := 5000 137 if offset > skip { 138 skip = offset 139 } 140 query = query.Where("id >= ?", skip).Order("id asc") 141 case RandomSort: 142 // Randomizing order with seed. https://stackoverflow.com/a/24511461 143 randSource := rand.NewSource(seed) 144 newRand := rand.New(randSource) 145 multiplier := newRand.Float64() 146 query = query.Order(fmt.Sprintf("(substr(id * %v, length(id) + 2))", multiplier)) 147 // Offset for random uses regular offset function of db because there is no easier way to skip results. 148 query = query.Offset(offset) 149 default: // Default is a catchall for "common" 150 query = query.Where("id >= ?", offset).Order("id asc") 151 } 152 return 153 } 154 155 // A surprisingly difficult function to find the criteria to match the Hidden Passions criteria. Some of the 156 // difficulty comes from the fact that negative numbers acting as exclusionary complicates the analysis. 157 func queryHiddenPassions(query *gorm.DB, name string, hiddenPassions []int, numberSystem NumberSystem) { 158 if len(hiddenPassions) == 0 { 159 return 160 } 161 type BuildQuery struct { 162 Left string 163 Comparator string 164 Right interface{} 165 Target int 166 } 167 // Prefix needed to pick column when querying the database. 168 prefix := string(strings.ToLower(numberSystem.Name)[0]) 169 // RunCount all the numerological numbers. 170 currentCount, maxCount, _ := countNumerologicalNumbers(name, numberSystem) 171 // Sort the numbers from largest to smallest. This is important because we want negative numbers last. 172 sort.Sort(sort.Reverse(sort.IntSlice(hiddenPassions))) 173 // Select the largest number as the prime number that calculations are based off of. 174 prime := hiddenPassions[0] 175 176 // If the largest number is negative then all the numbers are negative. Process differently. 177 if prime < 0 { 178 for _, hp := range hiddenPassions { 179 var where *gorm.DB 180 hpCount, _ := currentCount[int32(-hp)] 181 // Loop through all valid numbers and find any one that is bigger than the negative prime we do not want. 182 for _, i := range numberSystem.ValidNumbers { 183 if inIntSlice(-i, hiddenPassions) { 184 continue 185 } 186 iCount, _ := currentCount[int32(i)] 187 leftCol := fmt.Sprintf("%v%d", prefix, i) 188 rightCol := fmt.Sprintf("%v%d", prefix, -hp) 189 190 if iCount-hpCount == 0 { // If modifier is '0' then no reason for unnecessary addition in query. 191 if where == nil { // If our Where clause hasn't been initialized yet then do it now. 192 where = DB.Where(fmt.Sprintf("%v > %v", leftCol, rightCol)) 193 } else { // if already initialized then add an Or clause 194 where.Or(fmt.Sprintf("%v > %v", leftCol, rightCol)) 195 } 196 } else { 197 if where == nil { // If our Where clause hasn't been initialized yet then do it now. 198 where = DB.Where(fmt.Sprintf("%v > %v - ?", leftCol, rightCol), iCount-hpCount) 199 } else { // if already initialized then add an Or clause 200 where.Or(fmt.Sprintf("%v > %v - ?", leftCol, rightCol), iCount-hpCount) 201 } 202 } 203 } 204 // Add grouped query to main query 205 query = query.Where(where) 206 } 207 return 208 } 209 210 // Get the count of the prime number. 211 primeCount, _ := currentCount[int32(prime)] 212 213 // Loop through all valid number system numbers and build the initial query values. 214 buildQuery := map[int]BuildQuery{} 215 for _, i := range numberSystem.ValidNumbers { 216 if i == prime { 217 // Column name is prefix + number. ex p1, p2 218 col := fmt.Sprintf("%v%d", prefix, i) 219 // The prime number count needs to be as large or larger than the current largest count. 220 buildQuery[i] = BuildQuery{col, ">=", maxCount - primeCount, 0} 221 } else { 222 // Column name is prefix + number. ex p1, p2 223 leftCol := fmt.Sprintf("%v%d", prefix, i) 224 rightCol := fmt.Sprintf("%v%d", prefix, prime) 225 // Get count of number 226 count, _ := currentCount[int32(i)] 227 // In order to satisfy search condition this number needs to be less than or equal to a target 228 // that is equal to the difference between the count of this number and the prime number 229 targetCount := primeCount - count 230 buildQuery[i] = BuildQuery{leftCol, "<=", rightCol, targetCount} 231 } 232 } 233 // Loop through numbers to adjust query. Skip the first number because it was handled as the prime number. 234 for _, p := range hiddenPassions[1:] { 235 if p >= 0 { 236 bq := buildQuery[p] 237 bq.Comparator = "=" 238 buildQuery[p] = bq 239 } else { 240 bq := buildQuery[-p] 241 bq.Comparator = "<" 242 buildQuery[-p] = bq 243 } 244 } 245 // Now that we have sufficiently modified the queries. Add them to the main query. 246 for _, v := range buildQuery { 247 if v.Target != 0 { 248 query = query.Where(fmt.Sprintf("%v %v %v + ?", v.Left, v.Comparator, v.Right), v.Target) 249 } else { 250 query = query.Where(fmt.Sprintf("%v %v %v", v.Left, v.Comparator, v.Right)) 251 } 252 } 253 } 254 255 func queryKarmicLessons(query *gorm.DB, name string, n []int, numberSystem NumberSystem) { 256 if len(n) == 0 { 257 return 258 } 259 // Prefix needed to pick column when querying the database. 260 prefix := string(strings.ToLower(numberSystem.Name)[0]) 261 // RunCount all the numerological numbers. 262 currentCount, _, _ := countNumerologicalNumbers(name, numberSystem) 263 for _, i := range n { 264 if i < 0 { 265 col := fmt.Sprintf("%v%d", prefix, -i) 266 count, _ := currentCount[int32(-i)] 267 query = query.Where(col+" + ? > 0", count) 268 } else { 269 col := fmt.Sprintf("%v%d", prefix, i) 270 count, _ := currentCount[int32(i)] 271 // This will either be 0 or less than 0. 272 // Less than 0 is impossible match and therefore acts to excludes these rows. 273 query = query.Where(col+" <= ?", 0-count) 274 } 275 } 276 } 277 278 // This function does all the heavy lifting for searching names. 279 func nameSearch(n string, numberSystem NumberSystem, masterNumbers []int, reduceWords bool, opts NameSearchOpts) (results []NameNumerology, offset int64, err error) { 280 requiredOpts := NameOpts{ 281 NumberSystem: numberSystem, 282 MasterNumbers: masterNumbers, 283 ReduceWords: reduceWords, 284 } 285 searchOpts := NameSearchOpts{ 286 Count: opts.Count, 287 Offset: opts.Offset, 288 Seed: opts.Seed, 289 Dictionary: opts.Dictionary, 290 Gender: opts.Gender, 291 Sort: opts.Sort, 292 Full: opts.Full, 293 Vowels: opts.Vowels, 294 Consonants: opts.Consonants, 295 HiddenPassions: opts.HiddenPassions, 296 KarmicLessons: opts.KarmicLessons, 297 Database: opts.Database, 298 } 299 300 if DB == nil { 301 err = connectToDatabase(opts.Database) 302 if err != nil { 303 return []NameNumerology{}, 0, err 304 } 305 } 306 307 table := strings.ToLower(opts.Dictionary) 308 // Make sure table is valid and has names. 309 var count int64 310 DB.Table(table).Count(&count) 311 if count == 0 { 312 return []NameNumerology{}, 0, fmt.Errorf("database table %v is empty", opts.Dictionary) 313 } 314 315 // Lowercase the number system name for use later. 316 nsName := strings.ToLower(numberSystem.Name) 317 318 splitNames := strings.Split(n, " ") 319 var nameToSearch string 320 for i, n := range splitNames { 321 if numberOfQuestionMarks(n) > 0 { 322 // Save the name for use later and replace it with just a ?. This will help the new name construction later. 323 nameToSearch = n 324 splitNames[i] = "?" 325 } 326 } 327 reconstructedName := strings.Join(splitNames, " ") 328 nonSearchNameResults := NameNumerology{reconstructedName, &requiredOpts, nil, nil, nil, nil} 329 330 // Begin constructing the query 331 if opts.Count == 0 { 332 opts.Count = 25 333 } 334 query := DB.Table(table).Select("min(id) as id, name").Group("name") 335 // Increase query count by 1 because we will use the last result as an indication that there are more results 336 // that can be paged through. The extra result will be dropped in the return. 337 query = query.Limit(opts.Count + 1) 338 339 // If there are letters around the ? then we need to do a LIKE search. 340 if len(nameToSearch) > 1 { 341 query = query.Where("LOWER(name) LIKE ?", strings.Replace(strings.ToLower(nameToSearch), "?", "%", -1)) 342 } 343 344 addQuerySort(query, opts.Sort, opts.Offset, opts.Seed) 345 addQueryGender(query, rune(opts.Gender)) 346 347 queryHiddenPassions(query, reconstructedName, opts.HiddenPassions, numberSystem) 348 queryKarmicLessons(query, reconstructedName, opts.KarmicLessons, numberSystem) 349 350 // Get the largest name value from the database and cache it so we don't have to look it up again. 351 var largestNameValueInDb int 352 var ok bool 353 if largestNameValueInDb, ok = largestNameValueInTable[table]; !ok { 354 if err := DB.Table(table).Select("max(max(pythagorean_full), max(chaldean_full)) as largest").Find(&largestNameValueInDb).Error; err != nil { 355 largestNameValueInDb = 100 356 } 357 largestNameValueInTable[table] = largestNameValueInDb 358 } 359 360 // Lookup for Full numbers 361 addQueryLookup(query, queryLookup{ 362 MinimumSearchNumber: nonSearchNameResults.Full().ReduceSteps[0], 363 MaximumSearchNumber: nonSearchNameResults.Full().ReduceSteps[0] + largestNameValueInDb, 364 TargetNumbers: opts.Full, 365 ColumnName: nsName + "_full", 366 MasterNumbers: masterNumbers, 367 ReduceWords: reduceWords, 368 }) 369 // Lookup for Vowel numbers 370 addQueryLookup(query, queryLookup{ 371 MinimumSearchNumber: nonSearchNameResults.Vowels().ReduceSteps[0], 372 MaximumSearchNumber: nonSearchNameResults.Vowels().ReduceSteps[0] + largestNameValueInDb, 373 TargetNumbers: opts.Vowels, 374 ColumnName: nsName + "_vowels", 375 MasterNumbers: masterNumbers, 376 ReduceWords: reduceWords, 377 }) 378 // Lookup for Consonant numbers 379 addQueryLookup(query, queryLookup{ 380 MinimumSearchNumber: nonSearchNameResults.Consonants().ReduceSteps[0], 381 MaximumSearchNumber: nonSearchNameResults.Consonants().ReduceSteps[0] + largestNameValueInDb, 382 TargetNumbers: opts.Consonants, 383 ColumnName: nsName + "_consonants", 384 MasterNumbers: masterNumbers, 385 ReduceWords: reduceWords, 386 }) 387 388 var selectedNames []precalculatedNumerology 389 func() { 390 query.Find(&selectedNames) 391 }() 392 393 results = []NameNumerology{} 394 for i, r := range selectedNames { 395 // In order to find out if there are more results, we search for 1 extra and make a note of its id 396 // to use as an offset later. Then exclude the final result from what is returned. 397 if len(selectedNames) <= opts.Count || i < len(selectedNames)-1 { 398 // Use reconstructedName because we want to replace the whole ? name, and not accidentally include additional letters. 399 // John Da? Doe would come out as John DaDavid Doe. reconstructedName avoids this. 400 newName := strings.Replace(reconstructedName, "?", r.Name, 1) 401 // Calculate the numerology results using the new full name. 402 results = append(results, NameNumerology{newName, &requiredOpts, &searchOpts, nil, nil, nil}) 403 } else { 404 offset = r.Id 405 } 406 } 407 // If sort is random then we need to derive the offset a different way. 408 if offset > 0 && strings.ToLower(opts.Sort) == RandomSort { 409 offset = int64(opts.Offset + opts.Count) 410 } 411 return results, offset, nil 412 }