~rbn/fit

eab46d6d4412cad3bad4186d930993169b53d469 — Ruben Schuller 1 year, 6 months ago
initial commit
8 files changed, 333 insertions(+), 0 deletions(-)

A bin.go
A cmd/fit/input.go
A cmd/fit/main.go
A fit.go
A go.mod
A item.go
A strategy.go
A strategy_test.go
A  => bin.go +30 -0
@@ 1,30 @@
package fit

type Bin struct {
	items    []Item
	size     int64
	capacity int64
}

func (b *Bin) add(i Item) {
	b.items = append(b.items, i)
	b.size += i.Size
}

func (b *Bin) fits(i Item) bool {
	return b.size+i.Size <= b.capacity
}

func newBin(capacity int64, i Item) Bin {
	return Bin{items: []Item{i}, size: i.Size, capacity: capacity}
}

func (b *Bin) Items() []Item {
	return b.items
}

type BinsBySize []Bin

func (x BinsBySize) Len() int           { return len(x) }
func (x BinsBySize) Less(i, j int) bool { return x[i].size < x[j].size }
func (x BinsBySize) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

A  => cmd/fit/input.go +36 -0
@@ 1,36 @@
package main

import (
	"bufio"
	"fmt"
	"io"
	"strconv"
	"strings"

	"sr.ht/~rbn/fit"
)

func parseDu(r io.Reader) ([]fit.Item, error) {
	items := []fit.Item{}

	s := bufio.NewScanner(r)
	for s.Scan() {
		fs := strings.SplitN(s.Text(), "\t", 2)
		if len(fs) != 2 {
			return nil, fmt.Errorf("invalid line in input: %v", s.Text())
		}

		size, err := strconv.ParseInt(fs[0], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("invalid size in input: %v", s.Text())
		}

		items = append(items, fit.Item{Size: size, Path: fs[1]})
	}

	if err := s.Err(); err != nil {
		return nil, fmt.Errorf("reading standard input: %v", err)
	}

	return items, nil
}

A  => cmd/fit/main.go +64 -0
@@ 1,64 @@
package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"sr.ht/~rbn/fit"
)

func printBins(bins []fit.Bin, w io.Writer) error {
	for _, bin := range bins {
		paths := []string{}
		for _, item := range bin.Items() {
			paths = append(paths, item.Path)
		}
		_, err := fmt.Fprintln(w, strings.Join(paths, "\t"))
		if err != nil {
			return err
		}
	}
	return nil
}

func main() {
	capacity := flag.Int64("capacity", 25*1024*1024*1024, "bin capacity in bytes")
	strategyBest := flag.Bool("best", false, "best fit decreasing")
	strategyFirst := flag.Bool("first", false, "first fit decreasing")
	strategyWorst := flag.Bool("worst", false, "worst fit decreasing")
	flag.Parse()

	if *strategyBest && *strategyFirst && *strategyWorst {
		log.Fatal("multiple strategies selected")
	}

	if !(*strategyBest || *strategyFirst || *strategyWorst) {
		log.Fatal("no strategy selected")
	}

	var strategy fit.StrategyFunc
	switch {
	case *strategyBest:
		strategy = fit.Best
	case *strategyFirst:
		strategy = fit.First
	case *strategyWorst:
		strategy = fit.Worst
	}

	items, err := parseDu(os.Stdin)
	if err != nil {
		log.Fatal(err)
	}

	bins, err := fit.Pack(strategy, *capacity, items)
	if err != nil {
		log.Fatal(err)
	}

	printBins(bins, os.Stdout)
}

A  => fit.go +24 -0
@@ 1,24 @@
package fit

import (
	"fmt"
	"sort"
)

func Pack(strategy StrategyFunc, capacity int64, items []Item) ([]Bin, error) {
	sort.Sort(itemsBySize(items))
	bins := []Bin{}

	for len(items) > 0 {
		var x Item
		x, items = items[len(items)-1], items[:len(items)-1]

		if x.Size > capacity {
			return nil, fmt.Errorf("%v exceeds capacity: size: %v capacity: %v", x.Path, x.Size, capacity)
		}

		bins = strategy(bins, capacity, x)
	}

	return bins, nil
}

A  => go.mod +1 -0
@@ 1,1 @@
module sr.ht/~rbn/fit

A  => item.go +12 -0
@@ 1,12 @@
package fit

type Item struct {
	Path string
	Size int64
}

type itemsBySize []Item

func (x itemsBySize) Len() int           { return len(x) }
func (x itemsBySize) Less(i, j int) bool { return x[i].Size < x[j].Size }
func (x itemsBySize) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

A  => strategy.go +45 -0
@@ 1,45 @@
package fit

import (
	"sort"
)

func First(bins []Bin, capacity int64, x Item) []Bin {
	for i, b := range bins {
		if !b.fits(x) {
			continue
		}
		bins[i].add(x)
		return bins
	}
	bins = append(bins, newBin(capacity, x))
	return bins
}

func Best(bins []Bin, capacity int64, x Item) []Bin {
	sort.Sort(sort.Reverse(BinsBySize(bins)))
	for i, b := range bins {
		if !b.fits(x) {
			continue
		}
		bins[i].add(x)
		return bins
	}
	bins = append(bins, newBin(capacity, x))
	return bins
}

func Worst(bins []Bin, capacity int64, x Item) []Bin {
	sort.Sort(BinsBySize(bins))
	for i, b := range bins {
		if !b.fits(x) {
			continue
		}
		bins[i].add(x)
		return bins
	}
	bins = append(bins, newBin(capacity, x))
	return bins
}

type StrategyFunc func([]Bin, int64, Item) []Bin

A  => strategy_test.go +121 -0
@@ 1,121 @@
package fit

import (
	"testing"
)

type strategyTest struct {
	item Item
	bin  int
	pos  int
}

var firstTests = []strategyTest{
	{
		item: Item{Path: "a", Size: 8},
		bin:  0,
		pos:  0,
	},
	{
		item: Item{Path: "b", Size: 1},
		bin:  0,
		pos:  1,
	},
	{
		item: Item{Path: "c", Size: 3},
		bin:  1,
		pos:  0,
	},
	{
		item: Item{Path: "d", Size: 1},
		bin:  0,
		pos:  2,
	},
	{
		item: Item{Path: "e", Size: 4},
		bin:  1,
		pos:  1,
	},
}

// one must swap the bins in head, as they are sorted in place. this isn't nice.
var bestTests = []strategyTest{
	{
		item: Item{Path: "a", Size: 6},
		bin:  0,
		pos:  0,
	},
	{
		item: Item{Path: "b", Size: 8},
		bin:  1,
		pos:  0,
	},
	{
		item: Item{Path: "c", Size: 1},
		bin:  0,
		pos:  1,
	},
	{
		item: Item{Path: "d", Size: 2},
		bin:  1,
		pos:  1,
	},
	{
		item: Item{Path: "e", Size: 1},
		bin:  0,
		pos:  2,
	},
}

var worstTests = []strategyTest{
	{
		item: Item{Path: "a", Size: 6},
		bin:  0,
		pos:  0,
	},
	{
		item: Item{Path: "b", Size: 8},
		bin:  1,
		pos:  0,
	},
	{
		item: Item{Path: "c", Size: 1},
		bin:  0,
		pos:  1,
	},
	{
		item: Item{Path: "d", Size: 2},
		bin:  0,
		pos:  2,
	},
	{
		item: Item{Path: "e", Size: 1},
		bin:  0,
		pos:  1,
	},
}

func testStrategy(t *testing.T, strategy StrategyFunc, tests []strategyTest) {
	bins := []Bin{}

	for _, test := range tests {
		bins = strategy(bins, 10, test.item)
		t.Logf("%#v",bins)
		if bins[test.bin].Items()[test.pos] != test.item {
			t.Logf("%+v %+v", test.item, bins[test.bin].Items()[test.pos])
			t.Fail()
		}
	}
}

func TestFirst(t *testing.T) {
	testStrategy(t, First, firstTests)
}

func TestBest(t *testing.T) {
	testStrategy(t, Best, bestTests)
}

func TestWorst(t *testing.T) {
	testStrategy(t, Worst, worstTests)
}