diff --git a/go.mod b/go.mod index 1fbf6858a..44a54616a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/chromedp/chromedp v0.7.3 github.com/corona10/goimagehash v1.0.3 github.com/disintegration/imaging v1.6.0 - github.com/fvbommel/sortorder v1.0.2 + github.com/fvbommel/sortorder v1.1.0 github.com/go-chi/chi v4.0.2+incompatible github.com/golang-jwt/jwt/v4 v4.0.0 github.com/golang-migrate/migrate/v4 v4.15.0-beta.1 diff --git a/go.sum b/go.sum index 14bf5606a..75b2d679e 100644 --- a/go.sum +++ b/go.sum @@ -233,8 +233,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= -github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= -github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= diff --git a/pkg/sqlite/driver.go b/pkg/sqlite/driver.go index 5712c77c7..c67379d1b 100644 --- a/pkg/sqlite/driver.go +++ b/pkg/sqlite/driver.go @@ -5,7 +5,7 @@ import ( "database/sql/driver" "fmt" - "github.com/fvbommel/sortorder" + "github.com/fvbommel/sortorder/casefolded" sqlite3 "github.com/mattn/go-sqlite3" ) @@ -37,9 +37,9 @@ func (d *CustomSQLiteDriver) Open(dsn string) (driver.Conn, error) { } } - // COLLATE NATURAL_CS - Case sensitive natural sort - err := conn.RegisterCollation("NATURAL_CS", func(s string, s2 string) int { - if sortorder.NaturalLess(s, s2) { + // COLLATE NATURAL_CI - Case insensitive natural sort + err := conn.RegisterCollation("NATURAL_CI", func(s string, s2 string) int { + if casefolded.NaturalLess(s, s2) { return -1 } else { return 1 diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 590586b94..de840b283 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -1128,7 +1128,7 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F // special handling for path addFileTable() addFolderTable() - query.sortAndPagination += fmt.Sprintf(" ORDER BY folders.path %s, file_folder.path %[1]s, files.basename %[1]s", direction) + query.sortAndPagination += fmt.Sprintf(" ORDER BY COALESCE(folders.path, '') || COALESCE(file_folder.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI %s", direction) case "file_mod_time": sort = "mod_time" addFileTable() @@ -1136,10 +1136,13 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F case "title": addFileTable() addFolderTable() - query.sortAndPagination += " ORDER BY COALESCE(galleries.title, files.basename, basename(COALESCE(folders.path, ''))) COLLATE NATURAL_CS " + direction + ", file_folder.path " + direction + query.sortAndPagination += " ORDER BY COALESCE(galleries.title, files.basename, basename(COALESCE(folders.path, ''))) COLLATE NATURAL_CI " + direction + ", file_folder.path COLLATE NATURAL_CI " + direction default: query.sortAndPagination += getSort(sort, direction, "galleries") } + + // Whatever the sorting, always use title/id as a final sort + query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC" } func (qb *GalleryStore) filesRepository() *filesRepository { diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index d5bb4e852..2648c523d 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -1026,7 +1026,7 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod case "path": addFilesJoin() addFolderJoin() - sortClause = " ORDER BY folders.path " + direction + ", files.basename " + direction + sortClause = " ORDER BY COALESCE(folders.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI " + direction case "file_count": sortClause = getCountSort(imageTable, imagesFilesTable, imageIDColumn, direction) case "tag_count": @@ -1039,10 +1039,13 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod case "title": addFilesJoin() addFolderJoin() - sortClause = " ORDER BY COALESCE(images.title, files.basename) COLLATE NATURAL_CS " + direction + ", folders.path " + direction + sortClause = " ORDER BY COALESCE(images.title, files.basename) COLLATE NATURAL_CI " + direction + ", folders.path COLLATE NATURAL_CI " + direction default: sortClause = getSort(sort, direction, "images") } + + // Whatever the sorting, always use title/id as a final sort + sortClause += ", COALESCE(images.title, images.id) COLLATE NATURAL_CI ASC" } q.sortAndPagination = sortClause + getPagination(findFilter) diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index 1c591614d..212f350b7 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -310,14 +310,17 @@ func (qb *movieQueryBuilder) getMovieSort(findFilter *models.FindFilterType) str direction = findFilter.GetDirection() } + sortQuery := "" switch sort { - case "name": // #943 - override name sorting to use natural sort - return " ORDER BY " + getColumn("movies", sort) + " COLLATE NATURAL_CS " + direction case "scenes_count": // generic getSort won't work for this - return getCountSort(movieTable, moviesScenesTable, movieIDColumn, direction) + sortQuery += getCountSort(movieTable, moviesScenesTable, movieIDColumn, direction) default: - return getSort(sort, direction, "movies") + sortQuery += getSort(sort, direction, "movies") } + + // Whatever the sorting, always use name/id as a final sort + sortQuery += ", COALESCE(movies.name, movies.id) COLLATE NATURAL_CI ASC" + return sortQuery } func (qb *movieQueryBuilder) queryMovie(ctx context.Context, query string, args []interface{}) (*models.Movie, error) { diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index f288401d3..2cd9b7356 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -893,20 +893,23 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) st direction = findFilter.GetDirection() } - if sort == "tag_count" { - return getCountSort(performerTable, performersTagsTable, performerIDColumn, direction) - } - if sort == "scenes_count" { - return getCountSort(performerTable, performersScenesTable, performerIDColumn, direction) - } - if sort == "images_count" { - return getCountSort(performerTable, performersImagesTable, performerIDColumn, direction) - } - if sort == "galleries_count" { - return getCountSort(performerTable, performersGalleriesTable, performerIDColumn, direction) + sortQuery := "" + switch sort { + case "tag_count": + sortQuery += getCountSort(performerTable, performersTagsTable, performerIDColumn, direction) + case "scenes_count": + sortQuery += getCountSort(performerTable, performersScenesTable, performerIDColumn, direction) + case "images_count": + sortQuery += getCountSort(performerTable, performersImagesTable, performerIDColumn, direction) + case "galleries_count": + sortQuery += getCountSort(performerTable, performersGalleriesTable, performerIDColumn, direction) + default: + sortQuery += getSort(sort, direction, "performers") } - return getSort(sort, direction, "performers") + // Whatever the sorting, always use name/id as a final sort + sortQuery += ", COALESCE(performers.name, performers.id) COLLATE NATURAL_CI ASC" + return sortQuery } func (qb *PerformerStore) tagsRepository() *joinRepository { diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index a5e903653..ee58cb0e2 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -1435,7 +1435,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF // special handling for path addFileTable() addFolderTable() - query.sortAndPagination += fmt.Sprintf(" ORDER BY folders.path %s, files.basename %[1]s", direction) + query.sortAndPagination += fmt.Sprintf(" ORDER BY COALESCE(folders.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI %s", direction) case "perceptual_similarity": // special handling for phash addFileTable() @@ -1472,13 +1472,16 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF case "title": addFileTable() addFolderTable() - query.sortAndPagination += " ORDER BY COALESCE(scenes.title, files.basename) COLLATE NATURAL_CS " + direction + ", folders.path " + direction + query.sortAndPagination += " ORDER BY COALESCE(scenes.title, files.basename) COLLATE NATURAL_CI " + direction + ", folders.path COLLATE NATURAL_CI " + direction case "play_count": // handle here since getSort has special handling for _count suffix query.sortAndPagination += " ORDER BY scenes.play_count " + direction default: query.sortAndPagination += getSort(sort, direction, "scenes") } + + // Whatever the sorting, always use title/id as a final sort + query.sortAndPagination += ", COALESCE(scenes.title, scenes.id) COLLATE NATURAL_CI ASC" } func (qb *SceneStore) getPlayCount(ctx context.Context, id int) (int, error) { diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index af864df01..334c3eca1 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -82,10 +82,10 @@ func getSort(sort string, direction string, tableName string) string { colName = sort } if strings.Compare(sort, "name") == 0 { - return " ORDER BY " + colName + " COLLATE NOCASE " + direction + return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction } if strings.Compare(sort, "title") == 0 { - return " ORDER BY " + colName + " COLLATE NATURAL_CS " + direction + return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction } return " ORDER BY " + colName + " " + direction diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index b8f783de1..0b5ed7f2f 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -414,16 +414,21 @@ func (qb *studioQueryBuilder) getStudioSort(findFilter *models.FindFilterType) s direction = findFilter.GetDirection() } + sortQuery := "" switch sort { case "scenes_count": - return getCountSort(studioTable, sceneTable, studioIDColumn, direction) + sortQuery += getCountSort(studioTable, sceneTable, studioIDColumn, direction) case "images_count": - return getCountSort(studioTable, imageTable, studioIDColumn, direction) + sortQuery += getCountSort(studioTable, imageTable, studioIDColumn, direction) case "galleries_count": - return getCountSort(studioTable, galleryTable, studioIDColumn, direction) + sortQuery += getCountSort(studioTable, galleryTable, studioIDColumn, direction) default: - return getSort(sort, direction, "studios") + sortQuery += getSort(sort, direction, "studios") } + + // Whatever the sorting, always use name/id as a final sort + sortQuery += ", COALESCE(studios.name, studios.id) COLLATE NATURAL_CI ASC" + return sortQuery } func (qb *studioQueryBuilder) queryStudio(ctx context.Context, query string, args []interface{}) (*models.Studio, error) { diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index 9ad4abcaf..71edf1297 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -609,22 +609,25 @@ func (qb *tagQueryBuilder) getTagSort(query *queryBuilder, findFilter *models.Fi direction = findFilter.GetDirection() } - if findFilter.Sort != nil { - switch *findFilter.Sort { - case "scenes_count": - return getCountSort(tagTable, scenesTagsTable, tagIDColumn, direction) - case "scene_markers_count": - return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM scene_markers_tags WHERE tags.id = scene_markers_tags.tag_id)+(SELECT COUNT(*) FROM scene_markers WHERE tags.id = scene_markers.primary_tag_id) %s", getSortDirection(direction)) - case "images_count": - return getCountSort(tagTable, imagesTagsTable, tagIDColumn, direction) - case "galleries_count": - return getCountSort(tagTable, galleriesTagsTable, tagIDColumn, direction) - case "performers_count": - return getCountSort(tagTable, performersTagsTable, tagIDColumn, direction) - } + sortQuery := "" + switch sort { + case "scenes_count": + sortQuery += getCountSort(tagTable, scenesTagsTable, tagIDColumn, direction) + case "scene_markers_count": + sortQuery += fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM scene_markers_tags WHERE tags.id = scene_markers_tags.tag_id)+(SELECT COUNT(*) FROM scene_markers WHERE tags.id = scene_markers.primary_tag_id) %s", getSortDirection(direction)) + case "images_count": + sortQuery += getCountSort(tagTable, imagesTagsTable, tagIDColumn, direction) + case "galleries_count": + sortQuery += getCountSort(tagTable, galleriesTagsTable, tagIDColumn, direction) + case "performers_count": + sortQuery += getCountSort(tagTable, performersTagsTable, tagIDColumn, direction) + default: + sortQuery += getSort(sort, direction, "tags") } - return getSort(sort, direction, "tags") + // Whatever the sorting, always use name/id as a final sort + sortQuery += ", COALESCE(tags.name, tags.id) COLLATE NATURAL_CI ASC" + return sortQuery } func (qb *tagQueryBuilder) queryTag(ctx context.Context, query string, args []interface{}) (*models.Tag, error) { diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index e20fd5935..726c83b6f 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -44,7 +44,7 @@ export class ListFilterModel { public searchTerm: string = ""; public currentPage = DEFAULT_PARAMS.currentPage; public itemsPerPage = DEFAULT_PARAMS.itemsPerPage; - public sortDirection: SortDirectionEnum = SortDirectionEnum.Asc; + public sortDirection: SortDirectionEnum = DEFAULT_PARAMS.sortDirection; public sortBy?: string; public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode; public zoomIndex: number = 1; @@ -62,6 +62,9 @@ export class ListFilterModel { this.mode = mode; this.config = config; this.sortBy = defaultSort; + if (this.sortBy === "date") { + this.sortDirection = SortDirectionEnum.Desc; + } if (defaultDisplayMode !== undefined) { this.displayMode = defaultDisplayMode; } @@ -95,12 +98,19 @@ export class ListFilterModel { this.randomSeed = Number.parseInt(match[1], 10); } } - // #3193 - sortdir undefined means asc - this.sortDirection = - params.sortdir === "desc" - ? SortDirectionEnum.Desc - : SortDirectionEnum.Asc; - + if (params.sortdir !== undefined) { + this.sortDirection = + params.sortdir === "desc" + ? SortDirectionEnum.Desc + : SortDirectionEnum.Asc; + } else { + // #3193 - sortdir undefined means asc + // #3559 - unless sortby is date, then desc + this.sortDirection = + params.sortby === "date" + ? SortDirectionEnum.Desc + : SortDirectionEnum.Asc; + } if (params.disp !== undefined) { this.displayMode = params.disp; } @@ -294,7 +304,13 @@ export class ListFilterModel { : undefined, sortby: this.getSortBy(), sortdir: - this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, + this.sortBy === "date" + ? this.sortDirection === SortDirectionEnum.Asc + ? "asc" + : undefined + : this.sortDirection === SortDirectionEnum.Desc + ? "desc" + : undefined, disp: this.displayMode !== DEFAULT_PARAMS.displayMode ? String(this.displayMode) @@ -321,7 +337,13 @@ export class ListFilterModel { perPage: this.itemsPerPage, sortby: this.getSortBy(), sortdir: - this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, + this.sortBy === "date" + ? this.sortDirection === SortDirectionEnum.Asc + ? "asc" + : undefined + : this.sortDirection === SortDirectionEnum.Desc + ? "desc" + : undefined, disp: this.displayMode, q: this.searchTerm || undefined, z: this.zoomIndex, diff --git a/vendor/github.com/fvbommel/sortorder/.gitignore b/vendor/github.com/fvbommel/sortorder/.gitignore deleted file mode 100644 index c021733e2..000000000 --- a/vendor/github.com/fvbommel/sortorder/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so -# Folders -_obj -_test -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* -_testmain.go -*.exe -*.test -*.prof diff --git a/vendor/github.com/fvbommel/sortorder/README.md b/vendor/github.com/fvbommel/sortorder/README.md deleted file mode 100644 index 7ebcab1d1..000000000 --- a/vendor/github.com/fvbommel/sortorder/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# sortorder [![PkgGoDev](https://pkg.go.dev/badge/github.com/fvbommel/sortorder)](https://pkg.go.dev/github.com/fvbommel/sortorder) - - import "github.com/fvbommel/sortorder" - -Sort orders and comparison functions. diff --git a/vendor/github.com/fvbommel/sortorder/casefolded/README.md b/vendor/github.com/fvbommel/sortorder/casefolded/README.md new file mode 100644 index 000000000..fd2d84f2e --- /dev/null +++ b/vendor/github.com/fvbommel/sortorder/casefolded/README.md @@ -0,0 +1,12 @@ +# casefolded [![PkgGoDev](https://pkg.go.dev/badge/github.com/fvbommel/sortorder/casefolded)](https://pkg.go.dev/github.com/fvbommel/sortorder/casefolded) + + import "github.com/fvbommel/sortorder/casefolded" + +Case-folded sort orders and comparison functions. + +These sort characters as the lowest unicode value that is equivalent to that character, ignoring case. + +Not all Unicode special cases are supported. + +This is a separate sub-package because this needs to pull in the Unicode tables in the standard library, +which can add significantly to the size of binaries. diff --git a/vendor/github.com/fvbommel/sortorder/casefolded/natsort.go b/vendor/github.com/fvbommel/sortorder/casefolded/natsort.go new file mode 100644 index 000000000..90deddac2 --- /dev/null +++ b/vendor/github.com/fvbommel/sortorder/casefolded/natsort.go @@ -0,0 +1,194 @@ +package casefolded + +import ( + "unicode" + "unicode/utf8" +) + +// Natural implements sort.Interface to sort strings in natural order. This +// means that e.g. "abc2" < "abc12". +// +// This is the simple case-folded version, +// which means that letters are considered equal if strings.SimpleFold says they are. +// For example, "abc2" < "ABC12" < "abc100" and 'k' == '\u212a' (the Kelvin symbol). +// +// Non-digit sequences and numbers are compared separately. +// The former are compared rune-by-rune using the lowest equivalent runes, +// while digits are compared numerically +// (except that the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitations: +// - only ASCII digits (0-9) are considered. +// - comparisons are done on a rune-by-rune basis, +// so some special case equivalences like 'ß' == 'SS" are not supported. +// - Special cases like Turkish 'i' == 'İ' (and not regular dotless 'I') +// are not supported either. +type Natural []string + +func (n Natural) Len() int { return len(n) } +func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) } + +func isDigit(b rune) bool { return '0' <= b && b <= '9' } + +// caseFold returns the lowest-numbered rune equivalent to the parameter. +func caseFold(r rune) rune { + // Iterate until SimpleFold returns a lower value. + // This will be the lowest-numbered equivalent rune. + var prev rune = -1 + for r > prev { + prev, r = r, unicode.SimpleFold(r) + } + return r +} + +// NaturalLess compares two strings using natural ordering. This means that e.g. +// "abc2" < "abc12". +// +// This is the simple case-folded version, +// which means that letters are considered equal if strings.SimpleFold says they are. +// For example, "abc2" < "ABC12" < "abc100" and 'k' == '\u212a' (the Kelvin symbol). +// +// Non-digit sequences and numbers are compared separately. +// The former are compared rune-by-rune using the lowest equivalent runes, +// while digits are compared numerically +// (except that the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitations: +// - only ASCII digits (0-9) are considered. +// - comparisons are done on a rune-by-rune basis, +// so some special case equivalences like 'ß' == 'SS" are not supported. +// - Special cases like Turkish 'i' == 'İ' (and not regular dotless 'I') +// are not supported either. +func NaturalLess(str1, str2 string) bool { + // ASCII fast path. + idx1, idx2 := 0, 0 + for idx1 < len(str1) && idx2 < len(str2) { + c1, c2 := rune(str1[idx1]), rune(str2[idx2]) + + // Bail out to full Unicode support? + if c1|c2 >= utf8.RuneSelf { + goto hasUnicode + } + + dig1, dig2 := isDigit(c1), isDigit(c2) + switch { + case dig1 != dig2: // Digits before other characters. + return dig1 // True if LHS is a digit, false if the RHS is one. + case !dig1: // && !dig2, because dig1 == dig2 + // For ASCII it suffices to normalize letters to upper-case, + // because upper-cased ASCII compares lexicographically. + // Note: this does not account for regional special cases + // like Turkish dotted capital 'İ'. + + // Canonicalize to upper-case. + c1 = unicode.ToUpper(c1) + c2 = unicode.ToUpper(c2) + // Identical upper-cased ASCII runes are equal. + if c1 == c2 { + idx1++ + idx2++ + continue + } + return c1 < c2 + default: // Digits + // Eat zeros. + for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ { + } + for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ { + } + // Eat all digits. + nonZero1, nonZero2 := idx1, idx2 + for ; idx1 < len(str1) && isDigit(rune(str1[idx1])); idx1++ { + } + for ; idx2 < len(str2) && isDigit(rune(str2[idx2])); idx2++ { + } + // If lengths of numbers with non-zero prefix differ, the shorter + // one is less. + if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { + return len1 < len2 + } + // If they're equally long, string comparison is correct. + if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { + return nr1 < nr2 + } + // Otherwise, the one with less zeros is less. + // Because everything up to the number is equal, comparing the index + // after the zeros is sufficient. + if nonZero1 != nonZero2 { + return nonZero1 < nonZero2 + } + } + // They're identical so far, so continue comparing. + } + // So far they are identical. At least one is ended. If the other continues, + // it sorts last. + return len(str1) < len(str2) + +hasUnicode: + for idx1 < len(str1) && idx2 < len(str2) { + c1, delta1 := utf8.DecodeRuneInString(str1[idx1:]) + c2, delta2 := utf8.DecodeRuneInString(str2[idx2:]) + + dig1, dig2 := isDigit(c1), isDigit(c2) + switch { + case dig1 != dig2: // Digits before other characters. + return dig1 // True if LHS is a digit, false if the RHS is one. + case !dig1: // && !dig2, because dig1 == dig2 + idx1 += delta1 + idx2 += delta2 + // Fast path: identical runes are equal. + if c1 == c2 { + continue + } + // ASCII fast path: ASCII characters compare by their upper-case equivalent (if any) + // because 'A' < 'a', so upper-case them. + if c1 <= unicode.MaxASCII && c2 <= unicode.MaxASCII { + c1 = unicode.ToUpper(c1) + c2 = unicode.ToUpper(c2) + if c1 != c2 { + return c1 < c2 + } + continue + } + // Compare lowest equivalent characters. + c1 = caseFold(c1) + c2 = caseFold(c2) + if c1 == c2 { + continue + } + return c1 < c2 + default: // Digits + // Eat zeros. + for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ { + } + for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ { + } + // Eat all digits. + nonZero1, nonZero2 := idx1, idx2 + for ; idx1 < len(str1) && isDigit(rune(str1[idx1])); idx1++ { + } + for ; idx2 < len(str2) && isDigit(rune(str2[idx2])); idx2++ { + } + // If lengths of numbers with non-zero prefix differ, the shorter + // one is less. + if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { + return len1 < len2 + } + // If they're equally long, string comparison is correct. + if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { + return nr1 < nr2 + } + // Otherwise, the one with less zeros is less. + // Because everything up to the number is equal, comparing the index + // after the zeros is sufficient. + if nonZero1 != nonZero2 { + return nonZero1 < nonZero2 + } + } + // They're identical so far, so continue comparing. + } + // So far they are identical. At least one is ended. If the other continues, + // it sorts last. + return len(str1[idx1:]) < len(str2[idx2:]) +} diff --git a/vendor/github.com/fvbommel/sortorder/doc.go b/vendor/github.com/fvbommel/sortorder/doc.go deleted file mode 100644 index a7dd9585d..000000000 --- a/vendor/github.com/fvbommel/sortorder/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package sortorder implements sort orders and comparison functions. -// -// Currently, it only implements so-called "natural order", where integers -// embedded in strings are compared by value. -package sortorder // import "github.com/fvbommel/sortorder" diff --git a/vendor/github.com/fvbommel/sortorder/natsort.go b/vendor/github.com/fvbommel/sortorder/natsort.go deleted file mode 100644 index 66a52c712..000000000 --- a/vendor/github.com/fvbommel/sortorder/natsort.go +++ /dev/null @@ -1,76 +0,0 @@ -package sortorder - -// Natural implements sort.Interface to sort strings in natural order. This -// means that e.g. "abc2" < "abc12". -// -// Non-digit sequences and numbers are compared separately. The former are -// compared bytewise, while the latter are compared numerically (except that -// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") -// -// Limitation: only ASCII digits (0-9) are considered. -type Natural []string - -func (n Natural) Len() int { return len(n) } -func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] } -func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) } - -func isdigit(b byte) bool { return '0' <= b && b <= '9' } - -// NaturalLess compares two strings using natural ordering. This means that e.g. -// "abc2" < "abc12". -// -// Non-digit sequences and numbers are compared separately. The former are -// compared bytewise, while the latter are compared numerically (except that -// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") -// -// Limitation: only ASCII digits (0-9) are considered. -func NaturalLess(str1, str2 string) bool { - idx1, idx2 := 0, 0 - for idx1 < len(str1) && idx2 < len(str2) { - c1, c2 := str1[idx1], str2[idx2] - dig1, dig2 := isdigit(c1), isdigit(c2) - switch { - case dig1 != dig2: // Digits before other characters. - return dig1 // True if LHS is a digit, false if the RHS is one. - case !dig1: // && !dig2, because dig1 == dig2 - // UTF-8 compares bytewise-lexicographically, no need to decode - // codepoints. - if c1 != c2 { - return c1 < c2 - } - idx1++ - idx2++ - default: // Digits - // Eat zeros. - for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ { - } - for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ { - } - // Eat all digits. - nonZero1, nonZero2 := idx1, idx2 - for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ { - } - for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ { - } - // If lengths of numbers with non-zero prefix differ, the shorter - // one is less. - if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { - return len1 < len2 - } - // If they're equal, string comparison is correct. - if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { - return nr1 < nr2 - } - // Otherwise, the one with less zeros is less. - // Because everything up to the number is equal, comparing the index - // after the zeros is sufficient. - if nonZero1 != nonZero2 { - return nonZero1 < nonZero2 - } - } - // They're identical so far, so continue comparing. - } - // So far they are identical. At least one is ended. If the other continues, - // it sorts last. - return len(str1) < len(str2) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index bc780c6a5..c7b53d4f0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -143,9 +143,9 @@ github.com/doug-martin/goqu/v9/sqlgen # github.com/fsnotify/fsnotify v1.5.1 ## explicit; go 1.13 github.com/fsnotify/fsnotify -# github.com/fvbommel/sortorder v1.0.2 +# github.com/fvbommel/sortorder v1.1.0 ## explicit; go 1.13 -github.com/fvbommel/sortorder +github.com/fvbommel/sortorder/casefolded # github.com/go-chi/chi v4.0.2+incompatible ## explicit github.com/go-chi/chi