implement cycle renames

This commit is contained in:
itchyny 2020-01-07 23:39:39 +09:00
parent 02f7964dc5
commit 33d7af2229
2 changed files with 141 additions and 2 deletions

60
mmv.go
View file

@ -1,6 +1,11 @@
package mmv
import "os"
import (
"fmt"
"math/rand"
"os"
"path/filepath"
)
// Move multiple files.
func Move(files map[string]string) error {
@ -22,8 +27,59 @@ type rename struct {
func buildRenames(files map[string]string) ([]rename, error) {
rs := make([]rename, 0, 2*len(files))
vs := make(map[string]int, len(files))
revs := make(map[string]string, len(files))
for src, dst := range files {
rs = append(rs, rename{src, dst})
revs[dst] = src
}
var i int
for _, dst := range files {
if vs[dst] > 0 {
continue
}
i++
var cycle bool
for {
vs[dst] = i
if x, ok := files[dst]; ok {
dst = x
if vs[x] > 0 {
cycle = vs[x] == i
break
}
} else {
break
}
}
var tmp string
if cycle {
tmp = randomPath(filepath.Dir(dst))
rs = append(rs, rename{dst, tmp})
vs[dst]--
}
for {
if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) {
rs = append(rs, rename{src, dst})
if !cycle {
vs[dst] = i
}
dst = src
} else {
break
}
}
if cycle {
rs = append(rs, rename{tmp, dst})
}
}
return rs, nil
}
func randomPath(dir string) string {
for {
path := filepath.Join(dir, fmt.Sprint(rand.Uint64()))
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return path
}
}
}

View file

@ -16,6 +16,7 @@ func TestMove(t *testing.T) {
files map[string]string
contents map[string]string
expected map[string]string
cnt int
err error
}{
{
@ -27,6 +28,7 @@ func TestMove(t *testing.T) {
files: map[string]string{
"foo": "bar",
},
cnt: 1,
contents: map[string]string{
"foo": "0",
},
@ -40,6 +42,7 @@ func TestMove(t *testing.T) {
"foo": "qux",
"bar": "quxx",
},
cnt: 2,
contents: map[string]string{
"foo": "0",
"bar": "1",
@ -51,6 +54,84 @@ func TestMove(t *testing.T) {
"baz": "2",
},
},
{
name: "swap two files",
files: map[string]string{
"foo": "bar",
"bar": "foo",
},
cnt: 3,
contents: map[string]string{
"foo": "0",
"bar": "1",
"baz": "2",
},
expected: map[string]string{
"bar": "0",
"foo": "1",
"baz": "2",
},
},
{
name: "two swaps",
files: map[string]string{
"foo": "bar",
"bar": "foo",
"baz": "qux",
"qux": "baz",
},
cnt: 6,
contents: map[string]string{
"foo": "0",
"bar": "1",
"baz": "2",
"qux": "3",
},
expected: map[string]string{
"bar": "0",
"foo": "1",
"qux": "2",
"baz": "3",
},
},
{
name: "three files",
files: map[string]string{
"foo": "bar",
"bar": "baz",
"baz": "qux",
},
cnt: 3,
contents: map[string]string{
"foo": "0",
"bar": "1",
"baz": "2",
},
expected: map[string]string{
"bar": "0",
"baz": "1",
"qux": "2",
},
},
{
name: "cycle three files",
files: map[string]string{
"foo": "bar",
"bar": "baz",
"baz": "foo",
},
cnt: 4,
contents: map[string]string{
"foo": "0",
"bar": "1",
"baz": "2",
},
expected: map[string]string{
"bar": "0",
"baz": "1",
"foo": "2",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@ -59,6 +140,8 @@ func TestMove(t *testing.T) {
require.NoError(t, os.Chdir(dir))
require.NoError(t, err)
require.NoError(t, setupFiles(tc.contents))
rs, _ := buildRenames(tc.files)
assert.Equal(t, tc.cnt, len(rs))
require.NoError(t, Move(tc.files))
assert.Equal(t, tc.expected, fileContents("."))
})