// mateDesires provides functions to see if a profile passes a user's desires // This package is used for users to find their mate matches, and for hosts serving mate profiles which fulfill a provided criteria package mateDesires import "seekia/resources/worldLanguages" import "seekia/imported/geodist" import "seekia/internal/convertCurrencies" import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/genetics/companyAnalysis" import "strings" import "errors" import "slices" // We use this function to initialize the numericalDesiresMap and choiceDesiresMap func init(){ numericalDesiresMap = make(map[string]struct{}) for _, desireName := range numericalDesiresList{ numericalDesiresMap[desireName] = struct{}{} } choiceDesiresMap = make(map[string]struct{}) for _, desireName := range choiceDesiresList{ choiceDesiresMap[desireName] = struct{}{} } } func GetAllDesiresList(copyList bool)[]string{ if (copyList == false){ // List only needs to be copied if we are sorting and/or editing its elements afterwards return allDesiresList } listCopy := slices.Clone(allDesiresList) return listCopy } var allDesiresList []string = []string{ "ProfileLanguage", "PrimaryLocationCountry", "SearchTerms", "HasMessagedMe", // Never used in criteria "IHaveMessaged", // Never used in criteria "HasRejectedMe", // Never used in criteria "IsLiked", // Never used in criteria "IsIgnored", // Never used in criteria "IsMyContact", // Never used in criteria "Age", "Height", "Wealth", "WealthInGold", // Only used in criteria "Distance", // Never used in criteria "DistanceFrom", // Only used in criteria "Sex", "Sexuality", "BodyFat", "BodyMuscle", "EyeColor", "SkinColor", "HairColor", "HairTexture", "HasHIV", "HasGenitalHerpes", "FruitRating", "VegetablesRating", "NutsRating", "GrainsRating", "DairyRating", "SeafoodRating", "BeefRating", "PorkRating", "PoultryRating", "EggsRating", "BeansRating", "Fame", "AlcoholFrequency", "TobaccoFrequency", "CannabisFrequency", "GenderIdentity", "23andMe_AncestryComposition", "23andMe_AncestryComposition_Restrictive", "23andMe_MaternalHaplogroup", "23andMe_PaternalHaplogroup", "23andMe_NeanderthalVariants", "Language", "PetsRating", "DogsRating", "CatsRating", "OffspringProbabilityOfAnyMonogenicDisease", // Never used in criteria "MonogenicDisease_Cystic_Fibrosis_ProbabilityOfPassingAVariant", // Only used in criteria "MonogenicDisease_Sickle_Cell_Anemia_ProbabilityOfPassingAVariant", // Only used in criteria } // A desire being Numerical or Choice depends on some factors // For example, BodyFat/BodyMuscle are numerical attributes, but we treat them as choice desires // The end user only has to select between the 4 possible options (1/4, 2/4, 3/4, 4/4) // Some attributes are not numerical or choice, and require a more complex desire analysis func GetNumericalDesiresList(copyList bool)[]string{ if (copyList == false){ return numericalDesiresList } listCopy := slices.Clone(numericalDesiresList) return listCopy } // We use these maps to make lookups faster when checking to see if a desire is numerical/choice/other var numericalDesiresMap map[string]struct{} var choiceDesiresMap map[string]struct{} var numericalDesiresList []string = []string{ "Age", "Height", "Distance", "Wealth", "WealthInGold", "23andMe_NeanderthalVariants", "OffspringProbabilityOfAnyMonogenicDisease", "MonogenicDisease_Cystic_Fibrosis_ProbabilityOfPassingAVariant", "MonogenicDisease_Sickle_Cell_Anemia_ProbabilityOfPassingAVariant", } var choiceDesiresList []string = []string{ "ProfileLanguage", "HasMessagedMe", "IHaveMessaged", "HasRejectedMe", "IsLiked", "IsMyContact", "IsIgnored", "PrimaryLocationCountry", "Sex", "Sexuality", "BodyFat", "BodyMuscle", "HairColor", "HairTexture", "SkinColor", "EyeColor", "HasHIV", "HasGenitalHerpes", "GenderIdentity", "23andMe_MaternalHaplogroup", "23andMe_PaternalHaplogroup", "FruitRating", "VegetablesRating", "NutsRating", "GrainsRating", "DairyRating", "SeafoodRating", "BeefRating", "PorkRating", "PoultryRating", "EggsRating", "BeansRating", "Fame", "AlcoholFrequency", "TobaccoFrequency", "CannabisFrequency", "PetsRating", "DogsRating", "CatsRating", } // These are desires that are not used by the end user through the GUI // They are only used in Criteria, which are the mate desires sent to hosts func CheckIfDesireIsCriteriaOnly(desireName string)bool{ desireHasMonogenicDiseasePrefix := strings.HasPrefix(desireName, "MonogenicDisease_") if (desireHasMonogenicDiseasePrefix == true){ return true } if (desireName == "DistanceFrom"){ return true } if (desireName == "WealthInGold"){ return true } return false } // This function will tell us if a desire allows the use of the RequireResponse setting // If true, it also return an attribute that a profile must contain to check if they fulfill the desire //Outputs: // -bool: Desire allows the use of RequireResponse // -string: An attribute name we can check to see if a profile has provided the information that the desire relies upon // -There may be other attributes, but they will all be mandatory alongside the attribute we return func CheckIfDesireAllowsRequireResponse(desireName string)(bool, string){ switch desireName{ case "HasMessagedMe", "IHaveMessaged", "HasRejectedMe", "IsLiked", "IsMyContact", "IsIgnored":{ // These attributes are not constructed from information that is provided within the user's profile // Thus, we do not allow ResponseRequired for them because the fulfillsDesire status will be known for all users return false, "" } case "SearchTerms", "OffspringProbabilityOfAnyMonogenicDisease":{ // We do not allow RequireResponse for these attributes return false, "" } case "WealthInGold":{ return true, "Wealth" } case "Distance", "DistanceFrom":{ return true, "PrimaryLocationLatitude" } case "23andMe_AncestryComposition", "23andMe_AncestryComposition_Restrictive":{ return true, "23andMe_AncestryComposition" } } hasPrefix := strings.HasPrefix(desireName, "MonogenicDisease_") if (hasPrefix == true){ // We do not allow RequireResponse for these attributes return false, "" } return true, desireName } func CheckIfDesireIsNumerical(desireName string)bool{ _, isNumericalDesire := numericalDesiresMap[desireName] return isNumericalDesire } func GetDesireTitleFromDesireName(desireName string)(string, error){ switch desireName{ case "ProfileLanguage":{ return "Profile Language", nil } case "PrimaryLocationCountry":{ return "Country", nil } case "SearchTerms":{ return "Search Terms", nil } case "HasMessagedMe":{ return "Has Messaged Me", nil } case "IHaveMessaged":{ return "I Have Messaged", nil } case "HasRejectedMe":{ return "Has Rejected Me", nil } case "IsLiked":{ return "Is Liked", nil } case "IsIgnored":{ return "Is Ignored", nil } case "IsMyContact":{ return "Is My Contact", nil } case "Age":{ return "Age", nil } case "Height":{ return "Height", nil } case "Wealth":{ return "Wealth", nil } case "Distance":{ return "Distance", nil } case "Sex":{ return "Sex", nil } case "Sexuality":{ return "Sexuality", nil } case "Fame":{ return "Fame", nil } case "BodyFat":{ return "Body Fat", nil } case "BodyMuscle":{ return "Body Muscle", nil } case "EyeColor":{ return "Eye Color", nil } case "SkinColor":{ return "Skin Color", nil } case "HairColor":{ return "Hair Color", nil } case "HairTexture":{ return "Hair Texture", nil } case "HasHIV":{ return "Has HIV", nil } case "HasGenitalHerpes":{ return "Has Genital Herpes", nil } case "FruitRating":{ return "Fruit Rating", nil } case "VegetablesRating":{ return "Vegetables Rating", nil } case "NutsRating":{ return "Nuts Rating", nil } case "GrainsRating":{ return "Grains Rating", nil } case "DairyRating":{ return "Dairy Rating", nil } case "SeafoodRating":{ return "Seafood Rating", nil } case "BeefRating":{ return "Beef Rating", nil } case "PorkRating":{ return "Pork Rating", nil } case "PoultryRating":{ return "Poultry Rating", nil } case "EggsRating":{ return "Eggs Rating", nil } case "BeansRating":{ return "Beans Rating", nil } case "AlcoholFrequency":{ return "Alcohol Frequency", nil } case "TobaccoFrequency":{ return "Tobacco Frequency", nil } case "CannabisFrequency":{ return "Cannabis Frequency", nil } case "GenderIdentity":{ return "Gender Identity", nil } case "23andMe_AncestryComposition":{ return "23andMe Ancestry Composition", nil } case "23andMe_MaternalHaplogroup":{ return "23andMe Maternal Haplogroup", nil } case "23andMe_PaternalHaplogroup":{ return "23andMe Paternal Haplogroup", nil } case "23andMe_NeanderthalVariants":{ return "23andMe Neanderthal Variants", nil } case "OffspringProbabilityOfAnyMonogenicDisease":{ return "Offspring Probability Of Any Monogenic Disease", nil } case "Language":{ return "Language", nil } case "PetsRating":{ return "Pets Rating", nil } case "DogsRating":{ return "Dogs Rating", nil } case "CatsRating":{ return "Cats Rating", nil } } return "", errors.New("GetDesireTitleFromDesireName called with invalid desireName: " + desireName) } //Inputs: // -string: Desire name // -string: Desire type // -func(attributeName string)(bool, error): The function to retrieve any attribute from the profile //Outputs: // -bool: Any desire exists // -bool: Desire is valid // -Will be false if requestor is malicious. Example: non-numerical desired value for numerical desire // -We will not always verify that the desire is valid. // -We just need to be able to know when the desire is invalid, rather than throwing an error // -bool: User response exists (if response is possible) // -bool: Profile fulfills desire // -error func CheckIfMateProfileFulfillsDesire(desireName string, getAnyDesireFunction func(string)(bool, string, error), getAnyProfileAttributeFunction func(string)(bool, int, string, error))(bool, bool, bool, bool, error){ isNumericDesire := CheckIfDesireIsNumerical(desireName) if (isNumericDesire == true){ desireMinimumExists, minimumString, err := getAnyDesireFunction(desireName + "_Minimum") if (err != nil) { return false, false, false, false, err } desireMaximumExists, maximumString, err := getAnyDesireFunction(desireName + "_Maximum") if (err != nil) { return false, false, false, false, err } if (desireMinimumExists == false && desireMaximumExists == false){ return false, false, false, false, nil } //Outputs: // -bool: Value exists // -float64: Value // -error getAttributeValueToCompare := func()(bool, float64, error){ if (desireName == "Wealth"){ profileWealthExists, _, profileWealthInGold, err := getAnyProfileAttributeFunction("WealthInGold") if (err != nil) { return false, 0, err } if (profileWealthExists == false){ return false, 0, nil } profileWealthInGoldFloat64, err := helpers.ConvertStringToFloat64(profileWealthInGold) if (err != nil) { return false, 0, err } // We must convert their wealth to our desired currency for the comparison getMyWealthCurrencyCode := func()(string, error){ currencyExists, currencyCode, err := getAnyDesireFunction("WealthCurrency") if (err != nil) { return "", err } if (currencyExists == false){ return "USD", nil } return currencyCode, nil } myCurrencyCode, err := getMyWealthCurrencyCode() if (err != nil) { return false, 0, err } profileNetworkTypeExists, _, profileNetworkTypeString, err := getAnyProfileAttributeFunction("NetworkType") if (err != nil) { return false, 0, err } if (profileNetworkTypeExists == false){ return false, 0, errors.New("CheckIfMateProfileFulfillsDesire called with profile missing NetworkType.") } profileNetworkType, err := helpers.ConvertNetworkTypeStringToByte(profileNetworkTypeString) if (err != nil) { return false, 0, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid NetworkType: " + profileNetworkTypeString) } _, convertedProfileWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(profileNetworkType, profileWealthInGoldFloat64, myCurrencyCode) if (err != nil) { return false, 0, err } return true, convertedProfileWealth, nil } exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction(desireName) if (err != nil) { return false, 0, err } if (exists == false){ return false, 0, nil } profileAttributeValueFloat64, err := helpers.ConvertStringToFloat64(profileAttributeValue) if (err != nil) { return false, 0, err } return true, profileAttributeValueFloat64, nil } attributeValueKnown, attributeValueToCompare, err := getAttributeValueToCompare() if (err != nil) { return false, false, false, false, err } if (attributeValueKnown == false){ return true, true, false, false, nil } if (desireMinimumExists == true){ minimumFloat64, err := helpers.ConvertStringToFloat64(minimumString) if (err != nil) { // Desire is invalid return true, false, false, false, nil } if (attributeValueToCompare < minimumFloat64){ return true, true, true, false, nil } } if (desireMaximumExists == true){ maximumFloat64, err := helpers.ConvertStringToFloat64(maximumString) if (err != nil) { // Desire is invalid return true, false, false, false, nil } if (attributeValueToCompare > maximumFloat64){ return true, true, true, false, nil } } return true, true, true, true, nil } _, isChoiceDesire := choiceDesiresMap[desireName] if (isChoiceDesire == true){ myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction(desireName) if (err != nil) { return false, false, false, false, err } if (myDesireExists == false){ // Users must select at least one option for choice filtering to take effect. // Otherwise, users will get confused. // The user must specify they want non-canonical attributes using the "Other" choice return false, false, false, false, nil } myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction(desireName) if (err != nil) { return false, false, false, false, err } if (exists == false){ return true, true, false, false, nil } // This is the attribute of the profile // We are checking to see if this attribute fulfills our desires profileAttributeValueBase64 := encoding.EncodeBytesToBase64String([]byte(profileAttributeValue)) // The profile attribute must be one of the items in the choices list // Each choice is encoded in base64 fulfillsDesire := slices.Contains(myDesiredChoicesList, profileAttributeValueBase64) if (fulfillsDesire == true){ return true, true, true, true, nil } containsOther := slices.Contains(myDesiredChoicesList, "Other") if (containsOther == false){ return true, true, true, false, nil } // The user has selected Other // Some desires allow custom values as well as having some canonical attributes // We must allow a special choice called "Other" // If the "Other" choice is chosen, we will allow attributes except for the canonical attributes // We already checked for all explicit attributes, so all we must do is make sure the user's profile attribute is not canonical // If it is not, we know the user fulfills the desire // Here is an example: // Desire Options: Man, Woman, Other // User selects Other, does not select Man or Woman // This means the user wants to allow all responses except for "Man" and "Woman" getDesireCanonicalAttributesList := func()([]string, error){ switch desireName{ case "GenderIdentity":{ canonicalAttributesList := []string{"Man", "Woman"} return canonicalAttributesList, nil } case "23andMe_MaternalHaplogroup":{ knownMaternalHaplogroupsList := companyAnalysis.GetKnownMaternalHaplogroupsList_23andMe() return knownMaternalHaplogroupsList, nil } case "23andMe_PaternalHaplogroup":{ knownPaternalHaplogroupsList := companyAnalysis.GetKnownPaternalHaplogroupsList_23andMe() return knownPaternalHaplogroupsList, nil } case "Language":{ worldLanguagesList, err := worldLanguages.GetWorldLanguagePrimaryNamesList() if (err != nil) { return nil, err } return worldLanguagesList, nil } } return nil, errors.New("My Desired Choices List contains Other choice for invalid desire: " + desireName) } desireCanonicalAttributesList, err := getDesireCanonicalAttributesList() if (err != nil) { return false, false, false, false, err } profileAttributeIsCanonical := slices.Contains(desireCanonicalAttributesList, profileAttributeValue) if (profileAttributeIsCanonical == true){ // Profile contains a canonical attribute that the user did not select return true, true, true, false, nil } // User selected Other, and the profile has a custom value return true, true, true, true, nil } switch desireName{ case "23andMe_AncestryComposition", "23andMe_AncestryComposition_Restrictive":{ exists, ancestryCompositionDesire, err := getAnyDesireFunction(desireName) if (exists == false){ // The user has not selected any ancestry composition desires // We treat this as a desire being disabled return false, false, false, false, nil } exists, _, userAncestryComposition, err := getAnyProfileAttributeFunction("23andMe_AncestryComposition") if (err != nil) { return false, false, false, false, err } if (exists == false){ return true, true, false, false, nil } // These are the user's desired bounds continentMinimumBoundsMap, continentMaximumBoundsMap, regionMinimumBoundsMap, regionMaximumBoundsMap, subregionMinimumBoundsMap, subregionMaximumBoundsMap, err := ReadAncestryCompositionDesire_23andMe(ancestryCompositionDesire) if (err != nil){ //Desire is invalid return true, false, false, false, nil } userAttributeIsValid, userContinentPercentagesMap, userRegionPercentagesMap, userSubregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, userAncestryComposition) if (err != nil){ return false, false, false, false, err } if (userAttributeIsValid == false){ return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid 23andMe_AncestryComposition attribute.") } desireIsRestrictive := strings.HasSuffix(desireName, "_Restrictive") // Now we see if profile fulfills the desire type locationTypeStruct struct{ // These are the ancestry location percentages of the user for all locations of this locationType locationTypePercentagesMap map[string]float64 // These are the minimum bounds which we desire for all locations of this locationType locationTypeMinimumBoundsMap map[string]float64 // These are the maximum bounds which we desire for all location of this locationType locationTypeMaximumBoundsMap map[string]float64 } continentsObject := locationTypeStruct{ locationTypePercentagesMap: userContinentPercentagesMap, locationTypeMinimumBoundsMap: continentMinimumBoundsMap, locationTypeMaximumBoundsMap: continentMaximumBoundsMap, } regionsObject := locationTypeStruct{ locationTypePercentagesMap: userRegionPercentagesMap, locationTypeMinimumBoundsMap: regionMinimumBoundsMap, locationTypeMaximumBoundsMap: regionMaximumBoundsMap, } subregionsObject := locationTypeStruct{ locationTypePercentagesMap: userSubregionPercentagesMap, locationTypeMinimumBoundsMap: subregionMinimumBoundsMap, locationTypeMaximumBoundsMap: subregionMaximumBoundsMap, } locationTypeObjectsList := []locationTypeStruct{continentsObject, regionsObject, subregionsObject} for _, locationTypeObject := range locationTypeObjectsList{ userLocationTypePercentagesMap := locationTypeObject.locationTypePercentagesMap desiredMinimumBoundsMap := locationTypeObject.locationTypeMinimumBoundsMap desiredMaximumBoundsMap := locationTypeObject.locationTypeMaximumBoundsMap for locationName, locationPercentage := range userLocationTypePercentagesMap{ desiredMinimumBound, exists := desiredMinimumBoundsMap[locationName] if (exists == false){ if (desireIsRestrictive == true){ // Profile has a location that we have not explicitly allowed // User does not fulfill our desire return true, true, true, false, nil } // We do not desire this location // We skip it continue } desiredMaximumBound, exists := desiredMaximumBoundsMap[locationName] if (exists == false){ return false, false, false, false, errors.New("ReadAncestryCompositionDesire_23andMe returning maximumBoundsMap missing location from minimumBounds map") } if (locationPercentage >= desiredMinimumBound && locationPercentage <= desiredMaximumBound){ // Profile fulfills this location // If we are not in restrictive mode, then the user fulfills the desires // If we are in restrictive mode, then we must make sure the rest of the user's locations are desired if (desireIsRestrictive == false){ return true, true, true, true, nil } } } } // We have analyzed all locations // If we are in restrictive mode, this means that all of the user's profile locations were desired // If we are not in restrictive mode, it means that they did not fulfill any of our desired locations if (desireIsRestrictive == true){ return true, true, true, true, nil } return true, true, true, false, nil } case "DistanceFrom":{ desireExists, distanceFromDesire, err := getAnyDesireFunction("DistanceFrom") if (desireExists == false){ return false, false, false, false, nil } desiredDistanceRange, desiredCoordinates, delimiterFound := strings.Cut(distanceFromDesire, "@") if (delimiterFound == false){ // Desire value is malformed return true, false, false, false, nil } desiredDistanceMinimum, desiredDistanceMaximum, delimiterFound := strings.Cut(desiredDistanceRange, "$") if (delimiterFound == false){ // Desire value is malformed return true, false, false, false, nil } desiredDistanceMinimumFloat64, err := helpers.ConvertStringToFloat64(desiredDistanceMinimum) if (err != nil){ // Desire value is malformed return true, false, false, false, nil } if (desiredDistanceMinimumFloat64 < 0){ // Desire value is malformed return true, false, false, false, nil } desiredDistanceMaximumFloat64, err := helpers.ConvertStringToFloat64(desiredDistanceMaximum) if (err != nil){ // Desire value is malformed return true, false, false, false, nil } if (desiredDistanceMaximumFloat64 < desiredDistanceMinimumFloat64){ // Desire value is malformed return true, false, false, false, nil } desiredLatitude, desiredLongitude, delimiterFound := strings.Cut(desiredCoordinates, "+") if (delimiterFound == false){ // Desire value is malformed return true, false, false, false, nil } desiredLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(desiredLatitude) if (err != nil){ // Desire value is malformed return true, false, false, false, nil } desiredLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(desiredLongitude) if (err != nil){ // Desire value is malformed return true, false, false, false, nil } isValid := helpers.VerifyLatitude(desiredLocationLatitudeFloat64) if (isValid == false){ // Desire value is malformed return true, false, false, false, nil } isValid = helpers.VerifyLongitude(desiredLocationLongitudeFloat64) if (isValid == false){ // Desire value is malformed return true, false, false, false, nil } exists, _, userLocationLatitude, err := getAnyProfileAttributeFunction("PrimaryLocationLatitude") if (err != nil) { return false, false, false, false, err } if (exists == false){ // User has no location provided, desire status is unknown return true, true, false, false, nil } exists, _, userLocationLongitude, err := getAnyProfileAttributeFunction("PrimaryLocationLongitude") if (err != nil) { return false, false, false, false, err } if (exists == false){ return false, true, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing PrimaryLocationLatitude, but missing PrimaryLocationLongitude") } userLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(userLocationLatitude) if (err != nil){ return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid PrimaryLocationLatitude: " + userLocationLatitude) } userLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(userLocationLongitude) if (err != nil){ return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid PrimaryLocationLongitude: " + userLocationLongitude) } distanceInKilometers, err := geodist.GetDistanceBetweenCoordinates(desiredLocationLatitudeFloat64, desiredLocationLongitudeFloat64, userLocationLatitudeFloat64, userLocationLongitudeFloat64) if (err != nil){ return false, false, false, false, err } if (distanceInKilometers < desiredDistanceMinimumFloat64 || distanceInKilometers > desiredDistanceMaximumFloat64){ return true, true, true, false, nil } return true, true, true, true, nil } case "Language":{ myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction("Language") if (err != nil) { return false, false, false, false, err } if (myDesireExists == false){ return false, false, false, false, nil } myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction("Language") if (err != nil) { return false, false, false, false, err } if (exists == false){ return true, true, false, false, nil } //TODO: Add option for requiring a user to fulfill all of your chosen languages // For example, a user could only search for people who speak both English and French profileLanguageItemsList := strings.Split(profileAttributeValue, "+&") profileLanguagesList := make([]string, 0) for _, languageItem := range profileLanguageItemsList{ languageName, _, delimiterFound := strings.Cut(languageItem, "$") if (delimiterFound == false){ return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid Language attribute: Invalid item: " + languageItem) } languageNameBase64 := encoding.EncodeBytesToBase64String([]byte(languageName)) languageIsDesired := slices.Contains(myDesiredChoicesList, languageNameBase64) if (languageIsDesired == true){ // This user has a language which we desire return true, true, true, true, nil } profileLanguagesList = append(profileLanguagesList, languageName) } otherIsChosen := slices.Contains(myDesiredChoicesList, "Other") if (otherIsChosen == false){ return true, true, true, false, nil } // If the user has any non-canonical languages, we will consider this user as having fulfilled our desires canonicalLanguagesMap, err := worldLanguages.GetWorldLanguageObjectsMap() if (err != nil) { return false, false, false, false, err } for _, languageName := range profileLanguagesList{ _, exists := canonicalLanguagesMap[languageName] if (exists == false){ // This user has a non-canonical language, thus fulfilling our desire of "Other" return true, true, true, true, nil } } return true, true, true, false, nil } case "SearchTerms":{ myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction("SearchTerms") if (err != nil) { return false, false, false, false, err } if (myDesireExists == false){ return false, false, false, false, nil } myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") myDesiredSearchTermsList := make([]string, 0, len(myDesiredChoicesList)) for _, searchTermBase64 := range myDesiredChoicesList{ searchTerm, err := encoding.DecodeBase64StringToUnicodeString(searchTermBase64) if (err != nil){ return false, false, false, false, errors.New("My search term desires contains invalid term: " + searchTermBase64) } myDesiredSearchTermsList = append(myDesiredSearchTermsList, searchTerm) } profileAttributesToCheckList := []string{"Description", "Tags", "Hobbies", "Beliefs"} for _, attributeName := range profileAttributesToCheckList{ attributeExists, _, attributeValue, err := getAnyProfileAttributeFunction(attributeName) if (err != nil) { return false, false, false, false, err } if (attributeExists == false){ continue } for _, searchTerm := range myDesiredSearchTermsList{ containsTerm := strings.Contains(attributeValue, searchTerm) if (containsTerm == true){ // This user fulfills our search term desires. return true, true, true, true, nil } } } // None of our search terms were found in this user's profile // They do not fulfill our search term desires. return true, true, true, false, nil } } return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with unknown desire: " + desireName) } func CreateAncestryCompositionDesire_23andMe( continentMinimumBoundsMap map[string]float64, continentMaximumBoundsMap map[string]float64, regionMinimumBoundsMap map[string]float64, regionMaximumBoundsMap map[string]float64, subregionMinimumBoundsMap map[string]float64, subregionMaximumBoundsMap map[string]float64)(string, error){ if (len(continentMinimumBoundsMap) != len(continentMaximumBoundsMap)){ return "", errors.New("CreateAncestryCompositionDesire_23andMe called with continent bounds maps of differing length") } if (len(regionMinimumBoundsMap) != len(regionMaximumBoundsMap)){ return "", errors.New("CreateAncestryCompositionDesire_23andMe called with region bounds maps of differing length") } if (len(subregionMinimumBoundsMap) != len(subregionMaximumBoundsMap)){ return "", errors.New("CreateAncestryCompositionDesire_23andMe called with subregion bounds maps of differing length") } // We will round down all percentages to 1 decimal, because that is the highest precision that 23andMe offers // Seekia will round to 1 decimal whenever it displays the desire, so the user would not be able to see why they did not add up to 100. getLocationSection := func(locationMinimumBoundsMap map[string]float64, locationMaximumBoundsMap map[string]float64)(string, error){ if (len(locationMinimumBoundsMap) == 0){ return "None", nil } locationItemsList := make([]string, 0, len(locationMinimumBoundsMap)) for locationName, locationMinimumBound := range locationMinimumBoundsMap{ locationMaximumBound, exists := locationMaximumBoundsMap[locationName] if (exists == false){ return "", errors.New("locationMinimumBoundsMap contains location, but locationMaximumBoundsMap does not.") } minimumBoundString := helpers.ConvertFloat64ToStringRounded(locationMinimumBound, 1) maximumBoundString := helpers.ConvertFloat64ToStringRounded(locationMaximumBound, 1) locationItem := locationName + "$" + minimumBoundString + "@" + maximumBoundString locationItemsList = append(locationItemsList, locationItem) } locationSection := strings.Join(locationItemsList, "#") return locationSection, nil } continentsSection, err := getLocationSection(continentMinimumBoundsMap, continentMaximumBoundsMap) if (err != nil) { return "", err } regionsSection, err := getLocationSection(regionMinimumBoundsMap, regionMaximumBoundsMap) if (err != nil) { return "", err } subregionsSection, err := getLocationSection(subregionMinimumBoundsMap, subregionMaximumBoundsMap) if (err != nil) { return "", err } compositionDesire := continentsSection + "+" + regionsSection + "+" + subregionsSection _, _, _, _, _, _, err = ReadAncestryCompositionDesire_23andMe(compositionDesire) if (err != nil){ return "", errors.New("CreateAncestryCompositionDesire_23andMe failed: Result is invalid: " + err.Error()) } return compositionDesire, nil } // These maps will only contain bounds for locations with no sublocations // The maps will not contain the parent locations for each location //Outputs: // -map[string]float64: Continent Minimum bounds map // -map[string]float64: Continent Maximum bounds map // -map[string]float64: Region Minimum bounds map // -map[string]float64: Region maximum bounds map // -map[string]float64: Subregion minimum bounds map // -map[string]float64: Subregion maximum bounds map // -error (will return err if desire is invalid) func ReadAncestryCompositionDesire_23andMe(ancestryCompositionDesire string)(map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, error){ // An ancestry composition desire value is formatted as follows: // Continent section + "+" + Region Section + "+" + Sub-Region section // Continents section is made up of continent items or "None" // Region section is made up of region items or "None" // Subregion section is made up of subregion items or "None" // Each section's items are separated by "#" // Continent Item: Continent name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole // Region Item: Region name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole // Subregion Item: Subregion name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole // Each continent/region/subregion must be locations that have no sublocations // For example, "Unassigned" has no regions/subregions, so it is a continent that can be included // Any Continents/Regions/Subregions that do not exist are considered undesired // 0%-0% ranges are not permitted. All ranges must represent a non-zero desired proportion locationSectionsList := strings.Split(ancestryCompositionDesire, "+") if (len(locationSectionsList) != 3){ return nil, nil, nil, nil, nil, nil, errors.New("Invalid 23andMe ancestry composition desire: " + ancestryCompositionDesire) } continentMinimumBoundsMap := make(map[string]float64) continentMaximumBoundsMap := make(map[string]float64) regionMinimumBoundsMap := make(map[string]float64) regionMaximumBoundsMap := make(map[string]float64) subregionMinimumBoundsMap := make(map[string]float64) subregionMaximumBoundsMap := make(map[string]float64) continentsSection := locationSectionsList[0] regionsSection := locationSectionsList[1] subregionsSection := locationSectionsList[2] if (continentsSection == "None" && regionsSection == "None" && subregionsSection == "None"){ return nil, nil, nil, nil, nil, nil, errors.New("ReadAncestryCompositionDesire_23andMe called with desire containing all-None sections.") } addLocationSectionToMaps := func(locationSection string, minimumBoundsMap map[string]float64, maximumBoundsMap map[string]float64)error{ if (locationSection == "None"){ return nil } locationItemsList := strings.Split(locationSection, "#") for _, locationItem := range locationItemsList{ locationName, desiredRange, delimiterFound := strings.Cut(locationItem, "$") if (delimiterFound == false){ return errors.New("Invalid 23andMe ancestry composition desire Contains invalid continent item: " + locationName) } desiredRangeMinimum, desiredRangeMaximum, delimiterFound := strings.Cut(desiredRange, "@") if (delimiterFound == false){ return errors.New("Invalid 23andMe ancestry composition desire: contains invalid desired range: " + desiredRange) } desiredRangeMinimumFloat64, err := helpers.ConvertStringToFloat64(desiredRangeMinimum) if (err != nil){ return errors.New("Invalid 23andMe ancestry composition desire: contains invalid minimum range bound: " + desiredRangeMinimum) } desiredRangeMaximumFloat64, err := helpers.ConvertStringToFloat64(desiredRangeMaximum) if (err != nil){ return errors.New("Invalid 23andMe ancestry composition desire: contains invalid maximum range bound: " + desiredRangeMaximum) } _, exists := minimumBoundsMap[locationName] if (exists == true){ return errors.New("Invalid 23andMe ancestry composition desire: contains duplicate location name: " + locationName) } _, exists = maximumBoundsMap[locationName] if (exists == true){ return errors.New("Invalid 23andMe ancestry composition desire: contains duplicate location name: " + locationName) } if (desiredRangeMinimumFloat64 < 0 || desiredRangeMinimumFloat64 > 100){ return errors.New("Invalid 23andMe ancestry composition desire: minimum bound out of range: " + desiredRangeMinimum) } if (desiredRangeMaximumFloat64 < 0 || desiredRangeMaximumFloat64 > 100){ return errors.New("Invalid 23andMe ancestry composition desire: maximum bound out of range: " + desiredRangeMaximum) } if (desiredRangeMinimumFloat64 > desiredRangeMaximumFloat64){ return errors.New("Invalid 23andMe ancestry composition desire: minimum is greater than maximum.") } minimumBoundsMap[locationName] = desiredRangeMinimumFloat64 maximumBoundsMap[locationName] = desiredRangeMaximumFloat64 } return nil } err := addLocationSectionToMaps(continentsSection, continentMinimumBoundsMap, continentMaximumBoundsMap) if (err != nil) { return nil, nil, nil, nil, nil, nil, err } err = addLocationSectionToMaps(regionsSection, regionMinimumBoundsMap, regionMaximumBoundsMap) if (err != nil) { return nil, nil, nil, nil, nil, nil, err } err = addLocationSectionToMaps(subregionsSection, subregionMinimumBoundsMap, subregionMaximumBoundsMap) if (err != nil) { return nil, nil, nil, nil, nil, nil, err } return continentMinimumBoundsMap, continentMaximumBoundsMap, regionMinimumBoundsMap, regionMaximumBoundsMap, subregionMinimumBoundsMap, subregionMaximumBoundsMap, nil }