1119 lines
38 KiB
Go
1119 lines
38 KiB
Go
|
|
// 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
|
|
}
|
|
|
|
|
|
|