seekia/internal/profiles/userStatistics/userStatistics.go

801 lines
28 KiB
Go
Raw Normal View History

// userStatistics provides functions to generate statistics about stored user profiles.
// The user chooses the attribute(s), and the gui displays a chart with a button to view the statistics data.
// See statisticsGui.go to see the gui code.
package userStatistics
//TODO: Add the ability to control for confounding variables
// Example: Wealth, controlled for age and sex
import "seekia/internal/badgerDatabase"
import "seekia/internal/helpers"
import "seekia/internal/profiles/calculatedAttributes"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/attributeDisplay"
import "seekia/internal/translation"
import "slices"
import "strings"
import "errors"
import "math"
type StatisticsItem struct{
// The label for the statistics item
// For a bar chart, this represents the name of an X axis bar.
// For a donut chart, this represents the name of a donut slice
// Example: "Man", "100-200"
// This will never be translated, unless it is the Unknown/No Response item, in which case
// the label will be "Unknown"/"No Response" translated to the user's current app language
Label string
// This is the formatted, human readable version of the label
// This will be translated into the application language
// Sometimes, the LabelFormatted will be identical to Label
// This does not include units (Example: " days", " users")
// Example:
// -"1000000-2000000" -> "1 million-2 million"
LabelFormatted string
// The value corresponding to the label
// For a bar chart, this represents the Y axis value for a bar.
// For a donut chart, this represents the value (size) of one of the donut slices
// This will never be translated
// For example, the value could be 500 if 500 men responded Yes.
Value float64
// This is the formatted version of the value
// This does not include units (Example: " days", " users")
// Examples:
// -5 -> "5/10"
// -1500000000000 -> "1.5 trillion"
ValueFormatted string
}
//Outputs:
// -int: Number of users analyzed in statistics
// -[]StatisticsItem: Statistics items list (sorted, not grouped)
// -bool: Grouping performed
// -[]StatisticsItem: Grouped items list
// -func(float64)(string, error): Function to format y axis values
// -This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable
// -Example: "1000000" -> "1 million"
// -error
func GetUserStatisticsItemsLists_BarChart(identityType string,
networkType byte,
xAxisAttribute string,
xAxisIsNumerical bool,
formatXAxisValuesFunction func(string)(string, error),
xAxisUnknownLabel string,
yAxisAttribute string)(int, []StatisticsItem, bool, []StatisticsItem, func(float64)(string, error), error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, nil, false, nil, nil, errors.New("GetUserStatisticsItemsLists_BarChart called with invalid networkType: " + networkTypeString)
}
getYAxisRoundingPrecision := func()int{
if (yAxisAttribute == "Number Of Users"){
return 0
}
if (yAxisAttribute == "Average Height"){
return 0
}
if (yAxisAttribute == "Average Age"){
return 0
}
return 1
}
yAxisRoundingPrecision := getYAxisRoundingPrecision()
//Outputs:
// -int: Total analyzed users
// -[]StatisticsItem: Statistics items list
// -map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses)
// -bool: yAxisIsAverage
// -map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true)
// -func(float64)(string, error): Function to format statistics values
// -bool: Include a No Response/Unknown value item
// -This is only needed if at least 1 user did not respond to the X axis attribute on their profile.
// -StatisticsItem: The unknown value item.
// -error
getStatisticsItemsList := func()(int, []StatisticsItem, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, StatisticsItem, error){
//TODO: Add "Probability Of ..."
// This will allow viewing of choice attribute probabilities
// For example: Probability of being Male, probability of being Female, etc.
// We need to add this for each canonical option
if (yAxisAttribute == "Number Of Users"){
totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
formatValuesFunction := func(input float64)(string, error){
// input will be an whole number representing the number of users
result := helpers.ConvertFloat64ToStringRounded(input, 0)
return result, nil
}
if (numberOfUnknownValueUsers == 0){
return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, false, StatisticsItem{}, nil
}
unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers)
unknownItem := StatisticsItem{
Label: xAxisUnknownLabel,
LabelFormatted: xAxisUnknownLabel,
Value: float64(numberOfUnknownValueUsers),
ValueFormatted: unknownValueFormatted,
}
return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownItem, nil
}
userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ")
if (yAxisIsAverage == true){
// This function will return the attribute name
// We must convert the attribute title to the attribute name
getAttributeToGetAverageFor := func()string{
attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ")
if (attributeTitle == "Wealth"){
return "WealthInGold"
}
if (attributeTitle == "23andMe Neanderthal Variants"){
return "23andMe_NeanderthalVariants"
}
if (attributeTitle == "Body Fat"){
return "BodyFat"
}
if (attributeTitle == "Body Muscle"){
return "BodyMuscle"
}
if (attributeTitle == "Fruit Rating"){
return "FruitRating"
}
if (attributeTitle == "Vegetables Rating"){
return "VegetablesRating"
}
if (attributeTitle == "Nuts Rating"){
return "NutsRating"
}
if (attributeTitle == "Grains Rating"){
return "GrainsRating"
}
if (attributeTitle == "Dairy Rating"){
return "DairyRating"
}
if (attributeTitle == "Seafood Rating"){
return "SeafoodRating"
}
if (attributeTitle == "Beef Rating"){
return "BeefRating"
}
if (attributeTitle == "Pork Rating"){
return "PorkRating"
}
if (attributeTitle == "Poultry Rating"){
return "PoultryRating"
}
if (attributeTitle == "Eggs Rating"){
return "EggsRating"
}
if (attributeTitle == "Beans Rating"){
return "BeansRating"
}
if (attributeTitle == "Alcohol Frequency"){
return "AlcoholFrequency"
}
if (attributeTitle == "Tobacco Frequency"){
return "TobaccoFrequency"
}
if (attributeTitle == "Cannabis Frequency"){
return "CannabisFrequency"
}
if (attributeTitle == "Pets Rating"){
return "PetsRating"
}
if (attributeTitle == "Dogs Rating"){
return "DogsRating"
}
if (attributeTitle == "Cats Rating"){
return "CatsRating"
}
return attributeTitle
}
attributeToGetAverageFor := getAttributeToGetAverageFor()
totalAnalyzedUsers := 0
//Map structure: X-Axis attribute -> Number of users with this x-axis attribute who have a y-axis attribute response
responseCountsMap := make(map[string]int)
//Map structure: X-axis attribute response -> All Y-axis attribute responses summed for users with this x-axis attribute
responseSumsMap := make(map[string]float64)
// This stores a count of the number of users whose X axis value is unknown
// For example, if X axis is Age, this is the number of users who did not respond
numberOfUsersWithUnknownXAxisValue := 0
// This stores the total sum of all y axis values for users who have an unknown X axis value
// For example, if the X axis attribute is age, and the Y axis attribute is average height,
// this stores the sum of heights of all users who have no age on their profile.
usersWithUnknownXAxisValueYAxisValuesSum := float64(0)
for _, userIdentityHash := range userIdentityHashesToAnalyzeList{
profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
if (profileFound == false){
continue
}
userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
if (userIsDisabled == true){
continue
}
attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
if (attributeExists == false){
// This user did not respond to the attribute we are getting the average for
// We will not add them to the statistics maps
continue
}
totalAnalyzedUsers += 1
userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue)
if (err != nil) {
return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue)
}
attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
if (attributeFound == false){
// This user did not respond to the X axis attribute
// We calculate the average for users who do not respond and put it in its own category
numberOfUsersWithUnknownXAxisValue += 1
usersWithUnknownXAxisValueYAxisValuesSum += userAttributeToGetAverageForValueFloat64
continue
}
responseCountsMap[userXAxisAttributeValue] += 1
responseSumsMap[userXAxisAttributeValue] += userAttributeToGetAverageForValueFloat64
}
_, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap))
for attributeResponse, responsesCount := range responseCountsMap{
attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
allResponsesSum, exists := responseSumsMap[attributeResponse]
if (exists == false){
return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Response sums map missing attribute value")
}
averageValue := allResponsesSum/float64(responsesCount)
averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision)
averageValueFormatted, err := formatYAxisValuesFunction(averageValueString)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
newStatisticsItem := StatisticsItem{
Label: attributeResponse,
LabelFormatted: attributeResponseFormatted,
Value: averageValue,
ValueFormatted: averageValueFormatted,
}
statisticsItemsList = append(statisticsItemsList, newStatisticsItem)
}
// We use this function to format values after grouping, if grouping is needed
formatValuesFunction := func(input float64)(string, error){
inputString := helpers.ConvertFloat64ToStringRounded(input, yAxisRoundingPrecision)
valueFormatted, err := formatYAxisValuesFunction(inputString)
if (err != nil) { return "", err }
return valueFormatted, nil
}
if (numberOfUsersWithUnknownXAxisValue == 0){
return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, StatisticsItem{}, nil
}
unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue)
unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err }
// This item represents the average value for the yAxisAttribute for users who did not respond.
// For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value
// will represent the average wealth for users who did not provide Height on their profile.
unknownStatisticsItem := StatisticsItem{
Label: xAxisUnknownLabel,
LabelFormatted: xAxisUnknownLabel,
Value: unknownResponsesAverage,
ValueFormatted: unknownResponsesValueFormatted,
}
return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsItem, nil
}
return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute)
}
totalAnalyzedUsers, statisticsItemsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownItem, unknownValueItem, err := getStatisticsItemsList()
if (err != nil) { return 0, nil, false, nil, nil, err }
sortStatisticsItemsList(statisticsItemsList, xAxisIsNumerical)
// We now see if we need to group the items in the list together
// We do this if there are more than 10 categories
if (len(statisticsItemsList) <= 10){
// No grouping needed. We are done.
if (includeUnknownItem == true){
statisticsItemsList = append(statisticsItemsList, unknownValueItem)
}
return totalAnalyzedUsers, statisticsItemsList, false, nil, formatValuesFunction, nil
}
groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(10, statisticsItemsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction)
if (err != nil) { return 0, nil, false, nil, nil, err }
if (includeUnknownItem == true){
statisticsItemsList = append(statisticsItemsList, unknownValueItem)
groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem)
}
return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, formatValuesFunction, nil
}
//Outputs:
// -int: Number of users analyzed in statistics
// -[]StatisticsItem: Statistics items list (sorted, not grouped)
// -bool: Grouping performed
// -[]StatisticsItem: Grouped items list
// -error
func GetUserStatisticsItemsLists_DonutChart(identityType string,
networkType byte,
attributeToAnalyze string,
attributeIsNumerical bool,
formatAttributeLabelsFunction func(string)(string, error),
unknownLabelTranslated string)(int, []StatisticsItem, bool, []StatisticsItem, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, nil, false, nil, errors.New("GetUserStatisticsItemsLists_DonutChart called with invalid networkType: " + networkTypeString)
}
totalAnalyzedUsers, statisticsItemsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction)
if (err != nil) { return 0, nil, false, nil, err }
sortStatisticsItemsList(statisticsItemsList, attributeIsNumerical)
getUnknownValueItem := func()StatisticsItem{
numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders)
unknownValueItem := StatisticsItem{
Label: unknownLabelTranslated,
LabelFormatted: unknownLabelTranslated,
Value: float64(numberOfUnknownResponders),
ValueFormatted: numberOfUnknownRespondersString,
}
return unknownValueItem
}
if (len(statisticsItemsList) <= 8){
// No grouping needed.
if (numberOfUnknownResponders != 0){
unknownValueItem := getUnknownValueItem()
statisticsItemsList = append(statisticsItemsList, unknownValueItem)
}
return totalAnalyzedUsers, statisticsItemsList, false, nil, nil
}
formatValuesFunction := func(input float64)(string, error){
// input will always be the number of users who responded with the value
// Thus, input will always be an integer
result := helpers.ConvertFloat64ToStringRounded(input, 0)
return result, nil
}
groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(8, statisticsItemsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction)
if (err != nil) { return 0, nil, false, nil, err }
if (numberOfUnknownResponders != 0){
unknownValueItem := getUnknownValueItem()
statisticsItemsList = append(statisticsItemsList, unknownValueItem)
groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem)
}
return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, nil
}
// This function will return a statistics items list of the following format:
// "Label": Attribute name (Example: "Male")
// "Value": The number of users who responded with the attribute (in this example: "Male")
//
// All users of provided identityType who are not disabled will be analyzed
// -int: Number of analyzed users
// -[]StatisticsItem: Statistics items list (not sorted or grouped)
// -map[string]int: Response counts map (Response -> Number of responders)
// -int: Number of No Response/Unknown value responders
// -error
func getProfileAttributeCountStatisticsItemsList(identityType string,
networkType byte,
attributeName string,
formatLabelsFunction func(string)(string, error))(int, []StatisticsItem, map[string]int, int, error){
userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
if (err != nil) { return 0, nil, nil, 0, err }
totalAnalyzedUsers := 0
// Map structure: User Attribute response -> Number of users who responded with the response
responseCountsMap := make(map[string]int)
// This stores the number of users for whom we do not know their value
numberOfUnknownValueUsers := 0
for _, userIdentityHash := range userIdentityHashesToAnalyzeList{
profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
if (err != nil) { return 0, nil, nil, 0, err }
if (profileFound == false){
continue
}
userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
if (err != nil) { return 0, nil, nil, 0, err }
if (userIsDisabled == true){
continue
}
totalAnalyzedUsers += 1
attributeFound, _, attributeValue, err := getAnyUserAttributeValueFunction(attributeName)
if (err != nil) { return 0, nil, nil, 0, err }
if (attributeFound == false){
numberOfUnknownValueUsers += 1
continue
}
responseCountsMap[attributeValue] += 1
}
statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap))
for attributeResponse, numberOfUsers := range responseCountsMap{
attributeResponseFormatted, err := formatLabelsFunction(attributeResponse)
if (err != nil) { return 0, nil, nil, 0, err }
attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers)
newStatisticsItem := StatisticsItem{
Label: attributeResponse,
LabelFormatted: attributeResponseFormatted,
Value: float64(numberOfUsers),
ValueFormatted: attributeNumberOfUsersString,
}
statisticsItemsList = append(statisticsItemsList, newStatisticsItem)
}
return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, nil
}
func sortStatisticsItemsList(inputStatisticsItemsList []StatisticsItem, labelIsNumerical bool){
if (len(inputStatisticsItemsList) <= 1){
return
}
if (labelIsNumerical == true){
// We sort the items by label values in ascending order
// Example: Bar chart columns are ages, in order of youngest to oldest
compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{
itemALabel := itemA.Label
itemBLabel := itemB.Label
itemAFloat64, err := helpers.ConvertStringToFloat64(itemALabel)
if (err != nil) {
panic("Invalid statistics item: Item Label is not float: " + itemALabel)
}
itemBFloat64, err := helpers.ConvertStringToFloat64(itemBLabel)
if (err != nil) {
panic("Invalid statistics item: Item Label is not float: " + itemBLabel)
}
if (itemAFloat64 == itemBFloat64){
return 0
}
if (itemAFloat64 < itemBFloat64){
return -1
}
return 1
}
slices.SortFunc(inputStatisticsItemsList, compareItemsFunction)
return
}
// We sort the items by their values in descending order
compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{
itemAValue := itemA.Value
itemBValue := itemB.Value
if (itemAValue == itemBValue){
return 0
}
if (itemAValue < itemBValue){
return 1
}
return -1
}
slices.SortFunc(inputStatisticsItemsList, compareItemsFunction)
}
// This function will group a statistics items list.
// It will group Labels and their values to fit into a specified number of groups
// Example: "1","2","3","4" -> "1-2", "3-4"
//Inputs:
// -int: Maximum groups to create
// -[]StatisticsItem: Statistics items list to group
// -bool: Label is numerical
// -If it is, we will group labels into groups of numbers.
// -Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other
// -map[string]int
// -Response Counts map (Attribute response -> Number of responders with that response)
// -bool: Value is average
// -If value is average, we will combine the values of each group and find their average
// -Otherwise, we will find their sum
// -map[string]float64
// -Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true)
// -func(float64)(string, error): This is the function we use to format the values
//Outputs:
// -[]StatisticsItem: Grouped statistics items list
// -error
func getStatisticsItemsListGrouped(maximumGroupsToCreate int,
inputStatisticsItemsList []StatisticsItem,
labelIsNumerical bool,
responseCountsMap map[string]int,
valueIsAverage bool,
responseSumsMap map[string]float64,
formatValuesFunction func(float64)(string, error))([]StatisticsItem, error){
if (len(inputStatisticsItemsList) <= maximumGroupsToCreate){
return nil, errors.New("maximumGroupsToCreate is <= length of input statistics items list")
}
// We deep copy the statistics items list to retain the sorted version
// We need to retain both versions because the user can view the raw or grouped data in the GUI
statisticsItemsList := slices.Clone(inputStatisticsItemsList)
// We use this function to get the new value for a group of labels
getGroupValue := func(itemsToCombineList []StatisticsItem)(float64, error){
// This will count the total number of users who responded with the responses within this group
// Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green
totalRespondersCount := float64(0)
// This will store the sum for all response values within the group
// We only need to add to this sum if valueIsAverage == true
allReponsesSummed := float64(0)
for _, statisticsItem := range itemsToCombineList{
itemLabel := statisticsItem.Label
responderCount, exists := responseCountsMap[itemLabel]
if (exists == false){
return 0, errors.New("responseCountsMap missing label: " + itemLabel)
}
totalRespondersCount += float64(responderCount)
if (valueIsAverage == true){
yAxisAttributeResponsesSum, exists := responseSumsMap[itemLabel]
if (exists == false){
return 0, errors.New("responseSumsMap missing label: " + itemLabel)
}
allReponsesSummed += yAxisAttributeResponsesSum
}
}
if (valueIsAverage == false){
return totalRespondersCount, nil
}
// The value is an average
// We need to find the average for all of the user responses for the labels in the input list
// The Values in the inputStatisticsItemsList are averages
// We can't average out the averages, because that will not give us the true average
// We have to use the original sums for all group items and average them
if (totalRespondersCount == 0){
return 0, errors.New("totalRespondersCount is 0.")
}
value := allReponsesSummed/float64(totalRespondersCount)
return value, nil
}
if (labelIsNumerical == true){
maximumItemsPerCategory := int(math.Ceil(float64(len(statisticsItemsList))/float64(maximumGroupsToCreate)))
statisticsItemsListSublists, err := helpers.SplitListIntoSublists(statisticsItemsList, maximumItemsPerCategory)
if (err != nil) { return nil, err }
groupedItemsList := make([]StatisticsItem, 0, len(statisticsItemsListSublists))
for _, groupItemsListSublist := range statisticsItemsListSublists{
if (len(groupItemsListSublist) == 1){
// Sometimes, a group with 1 item will be created
// This happens if the groups cannot be evenly divided, so there is a remainder of 1.
// Example: 10->4 groups = 3, 3, 3, 1.
//TODO: Prevent this from happening so groups always have more than 1 subitem
groupItem := groupItemsListSublist[0]
groupedItemsList = append(groupedItemsList, groupItem)
continue
}
finalIndex := len(groupItemsListSublist)-1
initialItem := groupItemsListSublist[0]
finalItem := groupItemsListSublist[finalIndex]
initialLabel := initialItem.Label
initialLabelFormatted := initialItem.LabelFormatted
finalLabel := finalItem.Label
finalLabelFormatted := finalItem.LabelFormatted
groupValue, err := getGroupValue(groupItemsListSublist)
if (err != nil) { return nil, err }
groupValueFormatted, err := formatValuesFunction(groupValue)
if (err != nil) { return nil, err }
newGroupStatisticsItem := StatisticsItem{
Label: initialLabel + "-" + finalLabel,
LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted,
Value: groupValue,
ValueFormatted: groupValueFormatted,
}
groupedItemsList = append(groupedItemsList, newGroupStatisticsItem)
}
return groupedItemsList, nil
}
// Label is not numerical
// We combine all categories after the first maximumGroupsToCreate into a category called Other
itemsToKeep := statisticsItemsList[:maximumGroupsToCreate]
itemsToCombine := statisticsItemsList[maximumGroupsToCreate:]
otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other")
otherGroupValue, err := getGroupValue(itemsToCombine)
if (err != nil) { return nil, err }
otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue)
if (err != nil) { return nil, err }
otherGroupItem := StatisticsItem{
Label: "Other",
LabelFormatted: otherTranslated,
Value: otherGroupValue,
ValueFormatted: otherGroupValueFormatted,
}
groupedStatisticsItemsList := append(itemsToKeep, otherGroupItem)
return groupedStatisticsItemsList, nil
}
func getUserIdentityHashesToAnalyzeList(identityType string)([][16]byte, error){
allUserIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType)
if (err != nil) { return nil, err }
if (len(allUserIdentityHashesList) < 10000){
return allUserIdentityHashesList, nil
}
helpers.RandomizeListOrder(allUserIdentityHashesList)
identityHashesToAnalyzeList := allUserIdentityHashesList[:10000]
return identityHashesToAnalyzeList, nil
}
//Outputs:
// -bool: Profile Exists
// -func(string)(bool, int, string, error)
// -error
func getRetrieveAnyAttributeFromUserNewestProfileFunction(identityHash [16]byte, networkType byte)(bool, func(string)(bool, int, string, error), error){
newestProfileExists, profileVersion, _, _, _, newestProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType)
if (err != nil) { return false, nil, err }
if (newestProfileExists == false){
return false, nil, nil
}
getAnyAttributeFromUserNewestProfileFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestProfileMap)
if (err != nil) { return false, nil, err }
return true, getAnyAttributeFromUserNewestProfileFunction, nil
}