diff --git a/cmd/cli/plan.go b/cmd/cli/plan.go index 8e0c8a5..3885371 100644 --- a/cmd/cli/plan.go +++ b/cmd/cli/plan.go @@ -28,6 +28,18 @@ func plan() *cli.Command { Aliases: []string{"v"}, Value: false, }, + &cli.BoolFlag{ + Name: "left-pack", + Usage: "Use the left-pack algorithm to calculate partitions", + Aliases: []string{"l"}, + Value: false, + }, + &cli.BoolFlag{ + Name: "reset", + Usage: "Reset the database before planning partitions", + Aliases: []string{"r"}, + Value: false, + }, }, Action: func(c *cli.Context) error { @@ -42,6 +54,13 @@ func plan() *cli.Command { panic(fmt.Errorf("error migrating db: %w", err)) } + if c.Bool("reset") { + err = store.RemovePartitionAssignment() + if err != nil { + return fmt.Errorf("error resetting partitions in the db: %w", err) + } + } + size := int64(4600000000) // size of a single layer DVD targetSize := c.String("targetSize") switch targetSize { @@ -76,9 +95,20 @@ func plan() *cli.Command { fmt.Printf("Target Size: %v\n", humanize.Bytes(uint64(size))) fmt.Println("Calculating partitions...") - partitions, err := partitioner.CalculatePartitions(store, size) - if err != nil { - return fmt.Errorf("error calculating partitions: %w", err) + var partitions [][]types.FileMetadata + + if !c.Bool("left-pack") { + partitions, err = partitioner.CalculatePartitions(store, size) + if err != nil { + return fmt.Errorf("error calculating partitions: %w", err) + } + } + + if c.Bool("left-pack") { + partitions, err = partitioner.CalculatePartitionsLeftPack(store, size) + if err != nil { + return fmt.Errorf("error calculating partitions: %w", err) + } } for i, partition := range partitions { diff --git a/db/db.go b/db/db.go index 389edc1..e9dfcc3 100644 --- a/db/db.go +++ b/db/db.go @@ -19,6 +19,7 @@ type DB interface { RemoveFile(fileMetadata types.FileMetadata) error StoreFilePartition(fileMetadata types.FileMetadata) error GetTotalSize() (int64, error) + RemovePartitionAssignment() error GetFileCount() (int64, error) GetFiles() ([]types.FileMetadata, error) } @@ -120,6 +121,15 @@ func (d *store) StoreFilePartition(fileMetadata types.FileMetadata) error { return nil } +func (d *store) RemovePartitionAssignment() error { + query := `UPDATE files SET partitionId = ''` + _, err := d.db.Exec(query) + if err != nil { + return fmt.Errorf("error removing partition assignment | %w", err) + } + return nil +} + func (d *store) GetTotalSize() (int64, error) { var size int64 query := `SELECT SUM(size) FROM files` diff --git a/partitioner/partitioner.go b/partitioner/partitioner.go index fba4a44..c9c65ae 100644 --- a/partitioner/partitioner.go +++ b/partitioner/partitioner.go @@ -5,6 +5,7 @@ import ( "forever-files/db" "forever-files/types" "github.com/dustin/go-humanize" + "sort" ) func CalculatePartitions(store db.DB, targetSize int64) (partitions [][]types.FileMetadata, err error) { @@ -57,6 +58,81 @@ func CalculatePartitions(store db.DB, targetSize int64) (partitions [][]types.Fi // CalculatePartitionsLeftPack calculates the partitions efficiently by searching for files that fit the remaining space in each partition func CalculatePartitionsLeftPack(store db.DB, targetSize int64) (partitions [][]types.FileMetadata, err error) { - // TODO: implement this function - return nil, nil + totalSize, err := store.GetTotalSize() + if err != nil { + return nil, fmt.Errorf("error getting total size: %w", err) + } + if targetSize <= 0 { + targetSize = totalSize / 2 + } + fmt.Printf("Total Size: %v\n", totalSize) + fmt.Printf("Target Size: %v\n", targetSize) + + files, err := store.GetFiles() + if err != nil { + return nil, fmt.Errorf("error getting files: %w", err) + } + partitions = make([][]types.FileMetadata, 0) + partitionSize := int64(0) + partitionFiles := make([]types.FileMetadata, 0) + overSizedFiles := make([]types.FileMetadata, 0) + overSizedSize := int64(0) + + // sort files by size + sort.SliceStable(files, func(i, j int) bool { + return files[i].Size > files[j].Size + }) + + for _, file := range files { + if file.Size > targetSize { + overSizedFiles = append(overSizedFiles, file) + overSizedSize += file.Size + file.PartitionId = "-1" + } else { + // you've hit files that are smaller than the target size + break + } + } + + partitionIndex := int64(0) + // pick the largest file that fits in the partition's remaining space using sort.Search() + // for loop counting down for the size of the files slice + for i := len(files) - 1; i >= 0; i-- { + index := indexOfLargestFittingFile(files, targetSize-partitionSize) + if index == -1 { + // no files fit in the partition so move on to the next partition + partitions = append(partitions, partitionFiles) + partitionFiles = make([]types.FileMetadata, 0) + partitionSize = 0 + partitionIndex++ + index = indexOfLargestFittingFile(files, targetSize-partitionSize) + if index == -1 { + // no files fit in the new partition so break out of the loop + break + } + } + partitionFiles = append(partitionFiles, files[index]) + partitionSize += files[index].Size + files[index].PartitionId = fmt.Sprintf("%d", partitionIndex) + // remove the file from the slice + files = append(files[:index], files[index+1:]...) + } + if len(partitionFiles) > 0 { + partitions = append(partitions, partitionFiles) + } + fmt.Printf("Over Sized File Count: %v\n", len(overSizedFiles)) + fmt.Printf("Total Over Sized Size: %v\n", humanize.Bytes(uint64(overSizedSize))) + return partitions, nil +} + +func indexOfLargestFittingFile(files []types.FileMetadata, remainingSize int64) int { + // find the index of the largest file that fits in the remaining space + index := sort.Search(len(files), func(i int) bool { + return files[i].Size < remainingSize + }) + // if index is == len(files) then there are no files that fit + if index == len(files) { + return -1 + } + return index }