Search code examples
jsongohierarchy

How to group data correctly to get a tree structure?


In golang application I make query to such table:

| ID        | AGG_YEAR | AGG_MONTH | GENDER | AGE_RANGE | INCOME_RANGE | TOTAL |
|-----------|----------|-----------|--------|-----------|--------------|-------|
| 107502389 | 2019     | 7         | F      | 18_29     | 1000_2000    | 15    |
| 107502389 | 2019     | 7         | F      | 18_29     | 2000_4000    | 42    |
| 107502389 | 2019     | 7         | F      | 30_44     | 1000_2000    | 25    |
| 107502389 | 2019     | 7         | F      | 30_44     | 2000_4000    | 63    |
| 107502389 | 2019     | 7         | M      | 18_29     | 1000_2000    | 30    |
| 107502389 | 2019     | 7         | M      | 18_29     | 2000_4000    | 18    |
| 107502389 | 2019     | 7         | M      | 30_44     | 1000_2000    | 36    |
| 107502389 | 2019     | 7         | M      | 30_44     | 2000_4000    | 19    |

This table stores information about the total number of males and females with a certain wage level in a certain month. Usually, after a query to the database, each record is parsed separately one by one:

type Entry struct {
    ID int `json:"id"`
    AggYear int `json:"agg_year"`
    AggMonth int `json:"agg_month"`
    Gender string `json:"gender"`
    AgeRange string `json:"age_range"`
    IncomeRange string `json:"income_range"`
    Total int `json:"total"`
}

var entries []Entry

rows, err := database.Query("***"); if err != nil {
    fmt.Println(err)
    return
}

defer rows.Close()

for rows.Next() {
    var entry Entry

    if err = rows.Scan(&entry.ID, &entry.AggMethod, &entry.AggYear, &entry.AggMonth, &entry.Gender, &entry.AgeRange, &entry.IncomeRange, &entry.Total); err != nil {
        fmt.Println(err)
        return
    }

    entries = append(entries, entry)
}

type IncomeDetails struct {
    IncomeRange string `json:"income_range"`
    Total int `json:"total"`
}

type AgeDetails struct {
    AgeRange string `json:"age_range"`
    Details []IncomeDetails `json:"details"`
}

type GenderDetails struct {
    Gender string `json:"gender"`
    Details []AgeDetails `json:"details"`
}

type EntryDetails struct {
    AggYear int `json:"agg_year"`
    AggMonth int `json:"agg_month"`
    Details []GenderDetails `json:"details"`
}

type DataEntry struct {
    ID int `json:"id"`
    Details []EntryDetails `json:"details"`
}

var entryDetails []EntryDetails

i:= 0
for i < len(entries) {
    var genderDetails []GenderDetails
    aggYear := entries[i].AggYear
    aggMonth := entries[i].AggMonth

    for aggYear == entries[i].AggYear && aggMonth == entries[i].AggMonth {
        gender := entries[i].Gender
        var ageDetails []AgeDetails
        for gender == entries[i].Gender {
            ageRange := entries[i].AgeRange
            var incomeDetails []IncomeDetails

            for ageRange == entries[i].AgeRange && gender == entries[i].Gender { // <- runtime error: index out of range
                incomeDetail := IncomeDetails{entries[i].IncomeRange, entries[i].Total}
                incomeDetails = append(incomeDetails, incomeDetail)
                i++
            }
            ageDetail := AgeDetails{entries[i-1].AgeRange, incomeDetails}
            ageDetails = append(ageDetails, ageDetail)
            i++
        }
        genderDetail := GenderDetails{entries[i-1].Gender, ageDetails}
        genderDetails = append(genderDetails, genderDetail)
        i++
    }
    entryDetail := EntryDetails{entries[i-1].AggYear, entries[i-1].AggMonth, genderDetails}
    entryDetails = append(entryDetails, entryDetail)
    i++
}

I want to know the best way to group the following values as in the example below? I want to understand the sequence of actions. I will be grateful for any help.

[
    {
        "id": 107502389,
        "details": [
            {
                "agg_year": 2019,
                "agg_month": 7,
                "details": [
                    {
                        "gender": "F",
                        "details": [
                            {
                                "age_range": "18_29",
                                "details": [
                                    {
                                        "income_range": "1000_2000",
                                        "total": "15"
                                    },
                                    {
                                        "income_range": "2000_4000",
                                        "total": "42"
                                    },
                                ]
                            },
                            {
                                "age_range": "30_44",
                                "details": [
                                    {
                                        "income_range": "1000_2000",
                                        "total": "25"
                                    },
                                    {
                                        "income_range": "2000_4000",
                                        "total": "63"
                                    },
                                ]
                            }
                        ]
                    },
                    {
                        "gender": "M",
                        "details": [
                            {
                                "age_range": "18_29",
                                "details": [
                                    {
                                        "income_range": "1000_2000",
                                        "total": "30"
                                    },
                                    {
                                        "income_range": "2000_4000",
                                        "total": "18"
                                    },
                                ]
                            },
                            {
                                "age_range": "30_44",
                                "details": [
                                    {
                                        "income_range": "1000_2000",
                                        "total": "36"
                                    },
                                    {
                                        "income_range": "2000_4000",
                                        "total": "19"
                                    },
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

Solution

  • Usually, I would be updating the Structure according to the response i want. For example in your case it would be :

    type DataEntry struct {
        ID int `json:"id"`
        Details []EntryDetails `json:"details"`
    }
    
    type EntryDetails struct { 
        AggYear int `json:"agg_year"`
        AggMonth int `json:"agg_month"`
        Details []GenderDetails `json:"details"`
    }
    
    type GenderDetails struct {
        Gender string `json:"gender"`
        Details []AgeDetails `json:"details"`
    } 
    
    type AgeDetails struct {
        AgeRange string `json:"age_range"`
        Details []IncomeDetails `json:"details"`
    }
    
    type IncomeDetails struct { 
        IncomeRange string `json:"income_range"`
        Total int `json:"total"`
    }
    

    Dividing your code into smaller parts is always easier to read and maintain.

    Next part to add the details to the struct : you should Query your code according to the requirements to fill the struct one by one. For example: getID-'Entry struct' first, then getAggYear & getAggMonth for the ID-'EntryDetails struct' and so one.

    You can find the full working program here : https://play.golang.org/p/_pdb5y9Wd-O

    Enjoy !