Skip to content

Commit

Permalink
update sorting algorithms and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ayoisaiah committed Jul 20, 2024
1 parent c4a9294 commit e87c432
Show file tree
Hide file tree
Showing 15 changed files with 904 additions and 87 deletions.
4 changes: 4 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ or: FIND [REPLACE] [PATHS TO FILES AND DIRECTORIES...]`
Usage: "Same options as --sort but presents the matches in the reverse order.",
DefaultText: "<sort>",
},
&cli.BoolFlag{
Name: "sort-per-dir",
Usage: "Ensure sorting is done per directory",
},
&cli.BoolFlag{
Name: "string-mode",
Aliases: []string{"s"},
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Config struct {
Debug bool `json:"debug"`
Recursive bool `json:"recursive"`
ResetIndexPerDir bool `json:"reset_index_per_dir"`
SortPerDir bool `json:"sort_per_dir"`
}

// SetFindStringRegex compiles a regular expression for the
Expand Down Expand Up @@ -215,6 +216,7 @@ func (c *Config) setDefaultOpts(ctx *cli.Context) error {
c.Interactive = ctx.Bool("interactive")
c.FixConflictsPattern = ctx.String("fix-conflicts-pattern")
c.ResetIndexPerDir = ctx.Bool("reset-index-per-dir")
c.SortPerDir = ctx.Bool("sort-per-dir")

if c.FixConflictsPattern == "" {
c.FixConflictsPattern = "(%d)"
Expand Down
173 changes: 92 additions & 81 deletions internal/sortfiles/sortfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,54 @@ import (
"cmp"
"io/fs"
"os"
"path/filepath"
"slices"
"sort"
"time"

"gopkg.in/djherbis/times.v1"

"github.com/MagicalTux/natsort"
"github.com/pterm/pterm"

"github.com/ayoisaiah/f2/internal/file"
"github.com/ayoisaiah/f2/internal/timeutil"
)

// FilesBeforeDirs is used to sort files before directories to avoid renaming
// ForRenamingAndUndo is used to sort files before directories to avoid renaming
// conflicts. It also ensures that child directories are renamed before their
// parents and vice versa in undo mode.
func FilesBeforeDirs(changes []*file.Change, revert bool) []*file.Change {
sort.SliceStable(changes, func(i, j int) bool {
compareElement1 := changes[i]
compareElement2 := changes[j]
func ForRenamingAndUndo(changes []*file.Change, revert bool) {
slices.SortStableFunc(changes, func(a, b *file.Change) int {
// sort files before directories
if !a.IsDir && b.IsDir {
return -1
}

// sort parent directories before child directories in revert mode
if revert {
return len(compareElement1.BaseDir) < len(compareElement2.BaseDir)
}

// sort files before directories
if !compareElement1.IsDir {
return true
return cmp.Compare(len(a.BaseDir), len(b.BaseDir))
}

// sort child directories before parent directories
return len(compareElement1.BaseDir) > len(compareElement2.BaseDir)
return cmp.Compare(len(b.BaseDir), len(a.BaseDir))
})

return changes
}

// EnforceHierarchicalOrder ensures all files in the same directory are sorted before
// children directories.
// EnforceHierarchicalOrder ensures all files in the same directory are sorted
// before children directories.
func EnforceHierarchicalOrder(changes []*file.Change) {
slices.SortStableFunc(changes, func(a, b *file.Change) int {
return cmp.Compare(a.BaseDir, b.BaseDir)
lenA, lenB := len(a.BaseDir), len(b.BaseDir)
if lenA == lenB {
// Directories should come after files
if a.IsDir && !b.IsDir {
return -1
}

return cmp.Compare(a.Source, b.Source)
}

return cmp.Compare(lenA, lenB)
})
}

Expand All @@ -57,121 +63,126 @@ func ByTime(
changes []*file.Change,
sortName string,
reverseSort bool,
) ([]*file.Change, error) {
var err error

sort.SliceStable(changes, func(i, j int) bool {
compareElement1Path := changes[i].RelSourcePath
compareElement2Path := changes[j].RelSourcePath

var compareElement1, compareElement2 times.Timespec
compareElement1, err = times.Stat(compareElement1Path)
compareElement2, err = times.Stat(compareElement2Path)
sortPerDir bool,
) {
slices.SortStableFunc(changes, func(a, b *file.Change) int {
sourceA, errA := times.Stat(a.RelSourcePath)
sourceB, errB := times.Stat(b.RelSourcePath)

if errA != nil || errB != nil {
pterm.Error.Printfln(
"error getting file times info: %v, %v",
errA,
errB,
)
os.Exit(1)
}

var itime, jtime time.Time
aTime, bTime := sourceA.ModTime(), sourceB.ModTime()

switch sortName {
case timeutil.Mod:
itime = compareElement1.ModTime()
jtime = compareElement2.ModTime()
case timeutil.Birth:
itime = compareElement1.ModTime()
jtime = compareElement2.ModTime()

if compareElement1.HasBirthTime() {
itime = compareElement1.BirthTime()
if sourceA.HasBirthTime() {
aTime = sourceA.BirthTime()
}

if compareElement2.HasBirthTime() {
jtime = compareElement2.BirthTime()
if sourceB.HasBirthTime() {
bTime = sourceB.BirthTime()
}
case timeutil.Access:
itime = compareElement1.AccessTime()
jtime = compareElement2.AccessTime()
aTime = sourceA.AccessTime()
bTime = sourceB.AccessTime()
case timeutil.Change:
itime = compareElement1.ModTime()
jtime = compareElement2.ModTime()

if compareElement1.HasChangeTime() {
itime = compareElement1.ChangeTime()
if sourceA.HasChangeTime() {
aTime = sourceA.ChangeTime()
}

if compareElement2.HasChangeTime() {
jtime = compareElement2.ChangeTime()
if sourceB.HasChangeTime() {
bTime = sourceB.ChangeTime()
}
}

it, jt := itime.UnixNano(), jtime.UnixNano()
if sortPerDir &&
filepath.Dir(a.RelSourcePath) != filepath.Dir(b.RelSourcePath) {
return 0
}

if reverseSort {
return it < jt
return -cmp.Compare(aTime.UnixNano(), bTime.UnixNano())
}

return it > jt
return cmp.Compare(aTime.UnixNano(), bTime.UnixNano())
})

return changes, err
}

// BySize sorts the changes according to their file size.
func BySize(changes []*file.Change, reverseSort bool) ([]*file.Change, error) {
var err error
// BySize sorts the file changes in place based on their file size, either in
// ascending or descending order depending on the `reverseSort` flag.
func BySize(changes []*file.Change, reverseSort, sortPerDir bool) {
slices.SortStableFunc(changes, func(a, b *file.Change) int {
var fileInfoA, fileInfoB fs.FileInfo
fileInfoA, errA := os.Stat(a.RelSourcePath)
fileInfoB, errB := os.Stat(b.RelSourcePath)

sort.SliceStable(changes, func(i, j int) bool {
compareElement1Path := changes[i].RelSourcePath
compareElement2Path := changes[j].RelSourcePath
if errA != nil || errB != nil {
pterm.Error.Printfln("error getting file info: %v, %v", errA, errB)
os.Exit(1)
}

var compareElement1, compareElement2 fs.FileInfo
compareElement1, err = os.Stat(compareElement1Path)
compareElement2, err = os.Stat(compareElement2Path)
fileASize := fileInfoA.Size()
fileBSize := fileInfoB.Size()

isize := compareElement1.Size()
jsize := compareElement2.Size()
// Don't sort files in different directories relative to each other
if sortPerDir &&
filepath.Dir(a.RelSourcePath) != filepath.Dir(b.RelSourcePath) {
return 0
}

if reverseSort {
return isize > jsize
return int(fileBSize - fileASize)
}

return isize < jsize
return int(fileASize - fileBSize)
})

return changes, err
}

// Natural sorts the changes according to natural order (meaning numbers are
// interpreted naturally).
func Natural(changes []*file.Change, reverseSort bool) ([]*file.Change, error) {
// interpreted naturally). However, non-numeric characters are remain sorted in
// ASCII order.
func Natural(changes []*file.Change, reverseSort bool) {
sort.SliceStable(changes, func(i, j int) bool {
compareElement1 := changes[i].RelSourcePath
compareElement2 := changes[j].RelSourcePath
sourceA := changes[i].RelSourcePath
sourceB := changes[j].RelSourcePath

if reverseSort {
return !natsort.Compare(compareElement1, compareElement2)
return !natsort.Compare(sourceA, sourceB)
}

return natsort.Compare(compareElement1, compareElement2)
return natsort.Compare(sourceA, sourceB)
})

return changes, nil
}

// Changes is used to sort changes according to the configured sort value.
func Changes(
changes []*file.Change,
sortName string,
reverseSort bool,
) ([]*file.Change, error) {
sortPerDir bool,
) {
// TODO: EnforceHierarchicalOrder should be the default sort
if sortPerDir {
EnforceHierarchicalOrder(changes)
}

switch sortName {
case "natural":
return Natural(changes, reverseSort)
Natural(changes, reverseSort)
case "size":
return BySize(changes, reverseSort)
BySize(changes, reverseSort, sortPerDir)
case timeutil.Mod,
timeutil.Access,
timeutil.Birth,
timeutil.Change:
return ByTime(changes, sortName, reverseSort)
ByTime(changes, sortName, reverseSort, sortPerDir)
}

return changes, nil
}
Loading

0 comments on commit e87c432

Please sign in to comment.