diff --git a/client/src/app/core/services/recipe.service.ts b/client/src/app/core/services/recipe.service.ts index aaa3ea5..208f0fb 100644 --- a/client/src/app/core/services/recipe.service.ts +++ b/client/src/app/core/services/recipe.service.ts @@ -326,11 +326,13 @@ export class RecipeService { country: string, filename: string, sortKey: string, + ascending: boolean ): Promise> { return this._httpClient.post( environment.api + '/recipes/sort/' + country + '/' + filename , JSON.stringify({ - "sortKey": sortKey + "sortKey": sortKey, + "ascending": ascending }), { withCredentials: true, responseType: 'json' } ); diff --git a/client/src/app/features/recipes/recipes.component.ts b/client/src/app/features/recipes/recipes.component.ts index c37bd59..8a4a74a 100644 --- a/client/src/app/features/recipes/recipes.component.ts +++ b/client/src/app/features/recipes/recipes.component.ts @@ -638,19 +638,38 @@ export class RecipesComponent implements OnInit, OnDestroy, AfterViewInit { // activate sorting 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] ( await this._recipeService.sortRecipe( await this._recipeService.getCurrentCountry(), this._recipeService.getCurrentFile(), - header + header, + toggleAscend ) ).subscribe({ next: (data: any) => { if(data.status == 'OK'){ + console.log(data.result); + alert("refresh ... "); window.location.reload(); } } diff --git a/server/data/data.go b/server/data/data.go index cba4399..eef8028 100644 --- a/server/data/data.go +++ b/server/data/data.go @@ -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 diff --git a/server/routers/recipe.go b/server/routers/recipe.go index 2b91bfd..8292034 100644 --- a/server/routers/recipe.go +++ b/server/routers/recipe.go @@ -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, }) } diff --git a/server/services/recipe/recipe.go b/server/services/recipe/recipe.go index dd2acc2..c15b0e9 100644 --- a/server/services/recipe/recipe.go +++ b/server/services/recipe/recipe.go @@ -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 {