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

@ -326,11 +326,13 @@ export class RecipeService {
country: string, country: string,
filename: string, filename: string,
sortKey: string, sortKey: string,
ascending: boolean
): Promise<Observable<any>> { ): Promise<Observable<any>> {
return this._httpClient.post<any>( return this._httpClient.post<any>(
environment.api + '/recipes/sort/' + country + '/' + filename , environment.api + '/recipes/sort/' + country + '/' + filename ,
JSON.stringify({ JSON.stringify({
"sortKey": sortKey "sortKey": sortKey,
"ascending": ascending
}), }),
{ withCredentials: true, responseType: 'json' } { withCredentials: true, responseType: 'json' }
); );

View file

@ -638,19 +638,38 @@ export class RecipesComponent implements OnInit, OnDestroy, AfterViewInit {
// activate sorting // activate sorting
console.log('sortByHeader', header); console.log('sortByHeader', header);
// save toggle for each header
// get header if saved
let headerToggle = await AsyncStorage.getItem("sort_"+header);
let toggleAscend = true;
if(headerToggle == null || headerToggle == undefined){
await AsyncStorage.setItem("sort_"+header, "true");
} else if(headerToggle == "true") {
toggleAscend = false;
await AsyncStorage.setItem("sort_"+header, "false");
} else if(headerToggle == "false"){
toggleAscend = true;
await AsyncStorage.setItem("sort_"+header, "true");
}
// //
console.log("sort ", headerToggle, " to ", header);
// send to server [/recipe/sort] // send to server [/recipe/sort]
( (
await this._recipeService.sortRecipe( await this._recipeService.sortRecipe(
await this._recipeService.getCurrentCountry(), await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile(), this._recipeService.getCurrentFile(),
header header,
toggleAscend
) )
).subscribe({ ).subscribe({
next: (data: any) => { next: (data: any) => {
if(data.status == 'OK'){ if(data.status == 'OK'){
console.log(data.result);
alert("refresh ... ");
window.location.reload(); window.location.reload();
} }
} }

View file

@ -10,6 +10,7 @@ import (
"recipe-manager/models" "recipe-manager/models"
"recipe-manager/services/logger" "recipe-manager/services/logger"
"slices" "slices"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -17,6 +18,8 @@ import (
"reflect" "reflect"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/text/collate"
"golang.org/x/text/language"
) )
type RecipeWithTimeStamps struct { type RecipeWithTimeStamps struct {
@ -42,6 +45,133 @@ type DefaultByCountry struct {
DefaultFileVersion int 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 ( var (
countries = []helpers.CountryName{{ countries = []helpers.CountryName{{
CountryID: "tha", CountryID: "tha",
@ -228,12 +358,17 @@ func (d *Data) GetRecipe(countryID, filename string) *models.Recipe {
var cached_original_recipe models.Recipe var cached_original_recipe models.Recipe
err := d.redisClient.GetKeyTo(filename, &cached_original_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 { if err == nil && d.redisClient.HealthCheck() == nil {
d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdated", zap.Any("target", filename)) d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdated", zap.Any("target", filename))
return &cached_original_recipe return &cached_original_recipe
} }
// if equal, OK // if equal, OK
d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdatedRedisOffline", zap.Any("target", filename))
return d.CurrentRecipe[countryID] return d.CurrentRecipe[countryID]
} }
@ -921,114 +1056,213 @@ func (d *Data) GetCountryIDByName(countryName string) (string, error) {
// ------ sorting ------ // ------ sorting ------
// FIXME: sorting not working // 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 // Get recipe
recipe := d.GetRecipe(countryID, filename) recipe := d.GetRecipe(countryID, filename)
// error code // define default language priority
errorCode := 0 collator := collate.New(language.Thai)
emptiedComparators := make([]string, 0)
// Sort slices.SortStableFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
switch sort_by { switch sort_by {
case "Product Code": case "Name":
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int { av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Name")
bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Name")
if a.ProductCode == "" || b.ProductCode == "" { // add collator
errorCode = 1 if !ascending {
emptiedComparators = append(emptiedComparators, a.ProductCode+" !compare! "+b.ProductCode) return collator.CompareString(bv.String(), av.String())
} }
return collator.CompareString(av.String(), bv.String())
return strings.Compare(a.ProductCode, b.ProductCode) case "Other Name":
}) av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherName")
case "Name": bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherName")
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int { // add collator
if !ascending {
if a.Name == "" || b.Name == "" { return collator.CompareString(bv.String(), av.String())
errorCode = 2
emptiedComparators = append(emptiedComparators, a.Name+" !compare! "+b.Name)
} }
return collator.CompareString(av.String(), bv.String())
return strings.Compare(a.Name, b.Name) case "Description":
}) av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Description")
case "Other Name": bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Description")
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int { // add collator
if !ascending {
if a.OtherName == "" || b.OtherName == "" { return collator.CompareString(bv.String(), av.String())
errorCode = 3
emptiedComparators = append(emptiedComparators, a.OtherName+" !compare! "+b.OtherName)
} }
return collator.CompareString(av.String(), bv.String())
return strings.Compare(a.OtherName, b.OtherName) case "Other Description":
}) av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherDescription")
case "Description": bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherDescription")
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int { // add collator
if !ascending {
if a.Description == "" || b.Description == "" { return collator.CompareString(bv.String(), av.String())
errorCode = 4
emptiedComparators = append(emptiedComparators, a.Description+" !compare! "+b.Description)
} }
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) // // split by '-'
}) // codes_a := strings.Split(av.String(), "-")
case "Other Description": // codes_b := strings.Split(bv.String(), "-")
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int {
if a.OtherDescription == "" || b.OtherDescription == "" { // // ensure productCode len
errorCode = 5 // if len(codes_a) == len(codes_b) {
emptiedComparators = append(emptiedComparators, a.OtherDescription+" !compare! "+b.OtherDescription) // 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) // if num_a < num_b {
}) // if !ascending {
case "Last Updated": // return 1
slices.SortFunc(recipe.Recipe01, func(a, b models.Recipe01) int { // }
// parse date // return -1
layout := "02-Jan-2006 15:04:05" // } 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 == "" { inta, _ := strconv.Atoi(prea)
errorCode = 6 intb, _ := strconv.Atoi(preb)
emptiedComparators = append(emptiedComparators, a.ProductCode+":"+a.LastChange+" !compare! "+b.ProductCode+":"+b.LastChange)
}
timeA, err := time.Parse(layout, a.LastChange) if inta < intb {
if err != nil { if !ascending {
// fmt.Println("Parse error! not in layout format: ", a.LastChange) return 1
}
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
return -1 return -1
} else if a.LastChange == "" && b.LastChange == "" { } else if inta > intb {
errorCode = 0 if !ascending {
return -1
}
return 1
} else {
return 0 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 { // set to cache after sorted
errStatus := fmt.Errorf("ERR[%v]", errorCode) // case redis active
fmt.Println(errStatus) if d.redisClient.HealthCheck() == nil {
return errStatus, emptiedComparators 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 // merge

View file

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

View file

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