うちの本棚のレイアウトの最適化をGopherさんに考えてもらった

本が増えたときに本棚にきちんと入れるためのレイアウトを考えるのが大変なので、Gopherに頑張ってもらうようにした

# イメージ図
![gophercomplex5_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9094/6eb1d7c1-44bc-b0c0-4ddd-f1ddb09a3bf1.png)


# 設計

本棚のサイズはIKEAの[BILLY](https://www.ikea.com/jp/ja/catalog/products/10351568/)をベースにしています。
漫画だと1段に2列並べられて、1列22冊合計で44冊を1段に収納できました。

以降、1列を`group`、1段を`rack`と称することにします。

## 見送ったこと

出版社やブランドをまとめようと思いましたが、1日で作り上げるレベルではなさそうなのでやめました

# 作ったもの

## データ

CSVで所蔵データを作って、これをベースに本が棚に収まるようにグルーピングします。
以下がCSVの例です

```
Title,Count,Brand,Maker
ダンジョン飯,8,Beam Comix,ハルタ
はたらく細胞,5,シリウスKC,講談社
テラフォーマーズ,5,ヤングジャンプコミックス,集英社
...
```

## コード

出力は、とりあえずわかればいいので、 `spew.Dump` を使いました。

```go
package main

import (
	"os"
	"sort"

	"github.com/davecgh/go-spew/spew"
	"github.com/gocarina/gocsv"
)

type book struct {
	Title string `csv:"Title"` // 本のタイトル
	Count int    `csv:"Count"` // 同タイトルの所持数
	Brand string `csv:"Brand"` // 本のブランド (ヤングジャンプコミックスとか)
	Maker string `csv:"Maker"` // 本の出版社 (講談社とか)
}

type rack struct {
	Total int
	Items []book
}

var (
	// 1groupの最大数
	groupMax = 22
	// 1rackの最大数
	rackMax  = 44
)

func main() {
	books := []*book{}
	if err := loadCSVFile("books.csv", &books); err != nil {
		panic(err)
	}

	sort.Slice(books, func(i, j int) bool {
		return books[i].Count > books[j].Count
	})

	racks := []rack{}
	for _, b := range books {
		items := []book{}
		// なるべくgroup単位にしたいけど、所蔵数がgroupMaxを超えることがあるので、その場合は別のrackに入れる
		if b.Count > groupMax {
			items = append(items, *b)
			racks = append(racks, rack{
				Total: b.Count,
				Items: items,
			})
		} else {
			var isSet bool
			for i, r := range racks {
				if r.Total >= rackMax {
					continue
				}
				// 実際に入れるときに本棚の前後にまたがることの防止策
				if r.Total >= groupMax && r.Total+b.Count > rackMax {
					continue
				}
				r.Total += b.Count
				r.Items = append(r.Items, *b)
				racks[i] = r
				isSet = true
				break
			}
			if !isSet {
				items = append(items, *b)
				racks = append(racks, rack{
					Total: b.Count,
					Items: items,
				})
			}
		}
	}
	spew.Dump(racks)
}

func loadCSVFile(fp string, v interface{}) (err error) {
	f, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE, os.ModePerm)
	if err != nil {
		return
	}
	defer f.Close()
	return gocsv.UnmarshalFile(f, v)
}

```

## 実行結果

```go
([]main.rack) (len=4 cap=4) {
 (main.rack) {
  Total: (int) 43,
  Items: ([]main.book) (len=3 cap=4) {
   (main.book) {
    Title: (string) (len=24) "大使閣下の料理人",
    Count: (int) 25,
    Brand: (string) (len=17) "モーニングKC",
    Maker: (string) (len=9) "講談社"
   },
   (main.book) {
    Title: (string) (len=9) "グラメ",
    Count: (int) 13,
    Brand: (string) (len=24) "バンチコミックス",
    Maker: (string) (len=9) "新潮社"
   },
   (main.book) {
    Title: (string) (len=24) "テラフォーマーズ",
    Count: (int) 5,
    Brand: (string) (len=36) "ヤングジャンプコミックス",
    Maker: (string) (len=9) "集英社"
   }
  }
 },
 (main.rack) {
  Total: (int) 44,
  Items: ([]main.book) (len=3 cap=4) {
   (main.book) {
    Title: (string) (len=18) "華麗なる食卓",
    Count: (int) 24,
    Brand: (string) (len=36) "ヤングジャンプコミックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=18) "信長のシェフ",
    Count: (int) 12,
    Brand: (string) (len=24) "芳文社コミックス",
    Maker: (string) (len=9) "芳文社"
   },
   (main.book) {
    Title: (string) (len=18) "ダンジョン飯",
    Count: (int) 8,
    Brand: (string) (len=10) "Beam Comix",
    Maker: (string) (len=9) "ハルタ"
   }
  }
 },
 (main.rack) {
  Total: (int) 44,
  Items: ([]main.book) (len=4 cap=4) {
   (main.book) {
    Title: (string) (len=18) "バーテンダー",
    Count: (int) 21,
    Brand: (string) (len=39) "ジャンプコミックデラックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=21) "王様の仕立て屋",
    Count: (int) 10,
    Brand: (string) (len=39) "ジャンプコミックデラックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=24) "異世界居酒屋のぶ",
    Count: (int) 9,
    Brand: (string) (len=33) "角川コミックス・エース",
    Maker: (string) (len=12) "角川書店"
   },
   (main.book) {
    Title: (string) (len=45) "とんでもスキルで異世界放浪メシ",
    Count: (int) 4,
    Brand: (string) (len=24) "ガルドコミックス",
    Maker: (string) (len=21) "オーバーラップ"
   }
  }
 },
 (main.rack) {
  Total: (int) 36,
  Items: ([]main.book) (len=8 cap=8) {
   (main.book) {
    Title: (string) (len=15) "君のナイフ",
    Count: (int) 7,
    Brand: (string) (len=39) "ジャンプコミックデラックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=18) "はたらく細胞",
    Count: (int) 5,
    Brand: (string) (len=14) "シリウスKC",
    Maker: (string) (len=9) "講談社"
   },
   (main.book) {
    Title: (string) (len=27) "まどろみバーメイド",
    Count: (int) 5,
    Brand: (string) (len=24) "芳文社コミックス",
    Maker: (string) (len=9) "芳文社"
   },
   (main.book) {
    Title: (string) (len=1) "G",
    Count: (int) 5,
    Brand: (string) (len=36) "ヤングサンデーコミックス",
    Maker: (string) (len=9) "小学館"
   },
   (main.book) {
    Title: (string) (len=12) "死刑囚042",
    Count: (int) 5,
    Brand: (string) (len=36) "ヤングジャンプコミックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=27) "干物妹うまるちゃん",
    Count: (int) 3,
    Brand: (string) (len=36) "ヤングジャンプコミックス",
    Maker: (string) (len=9) "集英社"
   },
   (main.book) {
    Title: (string) (len=9) "電車男",
    Count: (int) 3,
    Brand: (string) (len=36) "ヤングサンデーコミックス",
    Maker: (string) (len=9) "小学館"
   },
   (main.book) {
    Title: (string) (len=12) "源君物語",
    Count: (int) 3,
    Brand: (string) (len=36) "ヤングジャンプコミックス",
    Maker: (string) (len=9) "集英社"
   }
  }
 }
}
```

## 後記

実際に本棚を並べ替えてみると、1冊のサイズの関係もあって、結果通りにいかないところもありましたが、ほぼほぼ問題ありませんでした。
1冊あたりの厚さ計算して並べ替えたほうがBetterっぽいです。