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  }