fix(sorting): 🐛 fix unchanged sorting result

This commit is contained in:
pakintada@gmail.com 2024-03-18 11:27:57 +07:00
parent ef9cf48fc1
commit b53183d884
5 changed files with 358 additions and 103 deletions

View file

@ -10,6 +10,7 @@ import (
"recipe-manager/models"
"recipe-manager/services/logger"
"slices"
"sort"
"strconv"
"strings"
"time"
@ -17,6 +18,8 @@ import (
"reflect"
"go.uber.org/zap"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
type RecipeWithTimeStamps struct {
@ -42,6 +45,133 @@ type DefaultByCountry struct {
DefaultFileVersion int
}
// sorting
type recipe01Sorter struct {
recipe01s []models.Recipe01
byKey func(r1, r2 *models.Recipe01) bool
}
// Len implements sort.Interface.
func (r *recipe01Sorter) Len() int {
return len(r.recipe01s)
}
// Less implements sort.Interface.
func (r *recipe01Sorter) Less(i int, j int) bool {
return r.byKey(&r.recipe01s[i], &r.recipe01s[j])
}
// Swap implements sort.Interface.
func (r *recipe01Sorter) Swap(i int, j int) {
r.recipe01s[i], r.recipe01s[j] = r.recipe01s[j], r.recipe01s[i]
}
type ByRecipe01Field func(r1, r2 *models.Recipe01) bool
func (by ByRecipe01Field) Sort(recipe01s []models.Recipe01) {
sorter_inst := &recipe01Sorter{
recipe01s: recipe01s,
byKey: by,
}
sort.Sort(sorter_inst)
// set back to cache
}
func SortRecipe01ByFieldName(field string, ascending bool) func(r1, r2 *models.Recipe01) bool {
return func(r1, r2 *models.Recipe01) bool {
// get value by reflect
v1 := reflect.Indirect(reflect.ValueOf(r1)).FieldByName(field)
v2 := reflect.Indirect(reflect.ValueOf(r2)).FieldByName(field)
asAscending := false
// ensure type is string
if v1.Kind() == reflect.String {
// check field name
if field == "LastChange" {
// special case, do date parser
// parse date
layout := "02-Jan-2006 15:04:05"
// noLeftLastChange := false
// noRightLastChange := false
time1, err := time.Parse(layout, v1.String())
if err != nil {
// noLeftLastChange = true
}
time2, err := time.Parse(layout, v2.String())
if err != nil {
// noRightLastChange = true
}
asAscending = time1.Before(time2)
} else {
asAscending = string(v1.String()) < string(v2.String())
}
}
if ascending {
return asAscending
}
// descending
return !asAscending
}
}
func partition(recipe01s []models.Recipe01, field string, low int, high int, isDate bool) ([]models.Recipe01, int) {
pivot := recipe01s[high]
pivotField := reflect.Indirect(reflect.ValueOf(pivot)).FieldByName(field)
init := low
for iter := low; iter < high; iter++ {
currentFieldIter := reflect.Indirect(reflect.ValueOf(recipe01s[iter])).FieldByName(field)
if !isDate {
if string(currentFieldIter.String()) < string(pivotField.String()) {
recipe01s[init], recipe01s[iter] = recipe01s[iter], recipe01s[init]
init++
}
} else {
// parse date
layout := "02-Jan-2006 15:04:05"
timePivot, _ := time.Parse(layout, pivotField.String())
timeIter, _ := time.Parse(layout, currentFieldIter.String())
if timeIter.Before(timePivot) {
recipe01s[init], recipe01s[iter] = recipe01s[iter], recipe01s[init]
init++
}
}
}
recipe01s[init], recipe01s[high] = recipe01s[high], recipe01s[init]
return recipe01s, init
}
func quickSortRecipe01(recipe01s []models.Recipe01, field string, low int, high int) []models.Recipe01 {
isDate := false
if field == "LastChange" {
isDate = true
}
if low < high {
var pivotIdx int
recipe01s, pivotIdx = partition(recipe01s, field, low, high, isDate)
recipe01s = quickSortRecipe01(recipe01s, field, low, pivotIdx-1)
recipe01s = quickSortRecipe01(recipe01s, field, pivotIdx+1, high)
}
return recipe01s
}
var (
countries = []helpers.CountryName{{
CountryID: "tha",
@ -228,12 +358,17 @@ func (d *Data) GetRecipe(countryID, filename string) *models.Recipe {
var cached_original_recipe models.Recipe
err := d.redisClient.GetKeyTo(filename, &cached_original_recipe)
// for index, v := range cached_original_recipe.Recipe01 {
// fmt.Println(index, " ", v.ProductCode)
// }
if err == nil && d.redisClient.HealthCheck() == nil {
d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdated", zap.Any("target", filename))
return &cached_original_recipe
}
// if equal, OK
d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdatedRedisOffline", zap.Any("target", filename))
return d.CurrentRecipe[countryID]
}
@ -921,114 +1056,213 @@ func (d *Data) GetCountryIDByName(countryName string) (string, error) {
// ------ sorting ------
// FIXME: sorting not working
func (d *Data) SortRecipe(countryID, filename string, sort_by string) (error, []string) {
func (d *Data) SortRecipe(countryID, filename string, sort_by string, ascending bool) {
// Get recipe
recipe := d.GetRecipe(countryID, filename)
// error code
errorCode := 0
emptiedComparators := make([]string, 0)
// define default language priority
collator := collate.New(language.Thai)
// Sort
switch sort_by {
case "Product Code":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
if a.ProductCode == "" || b.ProductCode == "" {
errorCode = 1
emptiedComparators = append(emptiedComparators, a.ProductCode+" !compare! "+b.ProductCode)
slices.SortStableFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
switch sort_by {
case "Name":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Name")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Name")
// add collator
if !ascending {
return collator.CompareString(bv.String(), av.String())
}
return strings.Compare(a.ProductCode, b.ProductCode)
})
case "Name":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
if a.Name == "" || b.Name == "" {
errorCode = 2
emptiedComparators = append(emptiedComparators, a.Name+" !compare! "+b.Name)
return collator.CompareString(av.String(), bv.String())
case "Other Name":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherName")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherName")
// add collator
if !ascending {
return collator.CompareString(bv.String(), av.String())
}
return strings.Compare(a.Name, b.Name)
})
case "Other Name":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
if a.OtherName == "" || b.OtherName == "" {
errorCode = 3
emptiedComparators = append(emptiedComparators, a.OtherName+" !compare! "+b.OtherName)
return collator.CompareString(av.String(), bv.String())
case "Description":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Description")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Description")
// add collator
if !ascending {
return collator.CompareString(bv.String(), av.String())
}
return strings.Compare(a.OtherName, b.OtherName)
})
case "Description":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
if a.Description == "" || b.Description == "" {
errorCode = 4
emptiedComparators = append(emptiedComparators, a.Description+" !compare! "+b.Description)
return collator.CompareString(av.String(), bv.String())
case "Other Description":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherDescription")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherDescription")
// add collator
if !ascending {
return collator.CompareString(bv.String(), av.String())
}
return collator.CompareString(av.String(), bv.String())
case "Product Code":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("ProductCode")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("ProductCode")
return strings.Compare(a.Description, b.Description)
})
case "Other Description":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
// // split by '-'
// codes_a := strings.Split(av.String(), "-")
// codes_b := strings.Split(bv.String(), "-")
if a.OtherDescription == "" || b.OtherDescription == "" {
errorCode = 5
emptiedComparators = append(emptiedComparators, a.OtherDescription+" !compare! "+b.OtherDescription)
}
// // ensure productCode len
// if len(codes_a) == len(codes_b) {
// for iter := 0; iter > len(codes_a)-1; iter++ {
// num_a, _ := strconv.Atoi(codes_a[iter])
// num_b, _ := strconv.Atoi(codes_b[iter])
return strings.Compare(a.OtherDescription, b.OtherDescription)
})
case "Last Updated":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
// parse date
layout := "02-Jan-2006 15:04:05"
// if num_a < num_b {
// if !ascending {
// return 1
// }
// return -1
// } else if num_b < num_a {
// if !ascending {
// return -1
// }
// return 1
// } else {
// continue
// }
// }
// }
prea := strings.ReplaceAll(av.String(), "-", "")
preb := strings.ReplaceAll(bv.String(), "-", "")
if a.LastChange == "" || b.LastChange == "" {
errorCode = 6
emptiedComparators = append(emptiedComparators, a.ProductCode+":"+a.LastChange+" !compare! "+b.ProductCode+":"+b.LastChange)
}
inta, _ := strconv.Atoi(prea)
intb, _ := strconv.Atoi(preb)
timeA, err := time.Parse(layout, a.LastChange)
if err != nil {
// fmt.Println("Parse error! not in layout format: ", a.LastChange)
errorCode = 7
emptiedComparators = append(emptiedComparators, a.ProductCode+":"+a.LastChange)
}
timeB, err := time.Parse(layout, b.LastChange)
if err != nil {
// fmt.Println("Parse error! not in layout format: ", b.LastChange)
errorCode = 8
emptiedComparators = append(emptiedComparators, b.ProductCode+":"+b.LastChange)
}
if a.LastChange == "" && b.LastChange != "" {
errorCode = 0
return 1
} else if a.LastChange != "" && b.LastChange == "" {
errorCode = 0
if inta < intb {
if !ascending {
return 1
}
return -1
} else if a.LastChange == "" && b.LastChange == "" {
errorCode = 0
} else if inta > intb {
if !ascending {
return -1
}
return 1
} else {
return 0
}
return timeA.Compare(timeB)
})
case "Last Updated":
av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("LastChange")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("LastChange")
layout := "02-Jan-2006 15:04:05"
timeA, _ := time.Parse(layout, av.String())
timeB, _ := time.Parse(layout, bv.String())
compare_result := 0
if !ascending {
compare_result = timeB.Compare(timeA)
} else {
compare_result = timeA.Compare(timeB)
}
if compare_result == 0 {
// has same time, compare product code
av2 := reflect.Indirect(reflect.ValueOf(a)).FieldByName("ProductCode")
bv2 := reflect.Indirect(reflect.ValueOf(b)).FieldByName("ProductCode")
prea := strings.ReplaceAll(av2.String(), "-", "")
preb := strings.ReplaceAll(bv2.String(), "-", "")
inta, _ := strconv.Atoi(prea)
intb, _ := strconv.Atoi(preb)
if inta < intb {
if !ascending {
return 1
}
return -1
} else if inta > intb {
if !ascending {
return -1
}
return 1
} else {
return 0
}
} else {
return compare_result
}
}
// no switching order
return 0
})
// Clean or re-sort
switch sort_by {
case "Product Code":
// ByRecipe01Field(SortRecipe01ByFieldName("ProductCode", ascending)).Sort(recipe.Recipe01)
// recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "ProductCode", 0, len(recipe.Recipe01)-1)
// collect product code and print
// for _, r := range recipe.Recipe01 {
// fmt.Println(r.ProductCode)
// }
// test sample batch size 10
result := quickSortRecipe01(recipe.Recipe01, "ProductCode", 0, 10)
// for index, r := range recipe.Recipe01 {
// fmt.Println(index, " ", r.ProductCode)
// }
// reassign
if len(result) == len(recipe.Recipe01) {
copy(recipe.Recipe01, result)
}
case "Name":
// ByRecipe01Field(SortRecipe01ByFieldName("Name", ascending)).Sort(recipe.Recipe01)
// recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "Name", 0, len(recipe.Recipe01)-1)
case "Other Name":
// ByRecipe01Field(SortRecipe01ByFieldName("OtherName", ascending)).Sort(recipe.Recipe01)
// recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "OtherName", 0, len(recipe.Recipe01)-1)
case "Description":
// ByRecipe01Field(SortRecipe01ByFieldName("Description", ascending)).Sort(recipe.Recipe01)
// recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "Description", 0, len(recipe.Recipe01)-1)
case "Other Description":
// ByRecipe01Field(SortRecipe01ByFieldName("OtherDecsription", ascending)).Sort(recipe.Recipe01)
// recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "OtherDecsription", 0, len(recipe.Recipe01)-1)
case "Last Updated":
// cleaning up empty last change
// for index, v := range recipe.Recipe01 {
// // save value temp
// fmt.Println("checking lastchange ", v.ProductCode, " ='", v.LastChange, "'")
// if v.LastChange == "" || len(v.LastChange) == 0 {
// fmt.Println("Shift @ ", index, " = ", v.ProductCode)
// temp := recipe.Recipe01[index]
// // copy(recipe.Recipe01[index:], recipe.Recipe01[index+1:])
// recipe.Recipe01 = append(recipe.Recipe01[:index], recipe.Recipe01[index+1:]...)
// recipe.Recipe01 = append(recipe.Recipe01, temp)
// }
// }
no_zero_idx := 0
for i := 0; i < len(recipe.Recipe01); i++ {
if recipe.Recipe01[i].LastChange != "" {
recipe.Recipe01[no_zero_idx], recipe.Recipe01[i] = recipe.Recipe01[i], recipe.Recipe01[no_zero_idx]
no_zero_idx++
}
}
}
if errorCode != 0 {
errStatus := fmt.Errorf("ERR[%v]", errorCode)
fmt.Println(errStatus)
return errStatus, emptiedComparators
// set to cache after sorted
// case redis active
if d.redisClient.HealthCheck() == nil {
fmt.Println("set to redis", filename)
d.redisClient.SetToKey(filename, recipe)
}
fmt.Println("set to server mem")
d.CurrentRecipe[countryID] = recipe
return nil, emptiedComparators
}
// merge

View file

@ -842,22 +842,20 @@ func (rr *RecipeRouter) sortRecipe(w http.ResponseWriter, r *http.Request) {
}
sortKey := sortConfig["sortKey"].(string)
ascending := sortConfig["ascending"].(bool)
// store cache
rr.cache_db.SetKeyTimeout(country+"_"+filename+"_latestSortKey", sortKey, 3600)
rr.cache_db.SetKeyTimeout(country+"_"+filename+"_"+sortKey, ascending, 3600)
// sort recipe
err, emptiedComparators := rr.data.SortRecipe(country, filename, sortKey)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.sortRecipe", zap.Error(err), zap.Any("emptiedComparators", emptiedComparators))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rr.data.SortRecipe(country, filename, sortKey, ascending)
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"key": sortKey,
"result": rr.data.GetRecipe(country, filename).Recipe01,
})
}

View file

@ -7,7 +7,6 @@ import (
"recipe-manager/models"
"recipe-manager/services/logger"
"sort"
"strings"
"go.uber.org/zap"
@ -302,7 +301,6 @@ func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequ
})
}
}
}
result.TotalCount = len(result.Result)
@ -310,9 +308,13 @@ func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequ
result.HasMore = result.TotalCount >= request.Take+request.Skip
if result.HasMore {
result.Result = result.Result[request.Skip : request.Take+request.Skip]
sort.Slice(result.Result, func(i, j int) bool {
return result.Result[i].ID < result.Result[j].ID
})
// sort.Slice(result.Result, func(i, j int) bool {
// return result.Result[i].ID < result.Result[j].ID
// })
// do sorting
} else if result.TotalCount > request.Skip {
result.Result = result.Result[request.Skip:]
} else {