packlist 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/Rakefile +12 -0
- data/app/assets/images/_consumed.svg.erb +5 -0
- data/app/assets/images/_worn.svg.erb +3 -0
- data/app/assets/stylesheets/_report.scss +201 -0
- data/app/assets/stylesheets/_system-font.scss +57 -0
- data/app/assets/stylesheets/styles.scss +2 -0
- data/app/assets/templates/report.html.erb +124 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/docs/sample_pack.png +0 -0
- data/docs/sample_pack.rb +68 -0
- data/exe/packlist +16 -0
- data/lib/packlist.rb +4 -0
- data/lib/packlist/dsl.rb +81 -0
- data/lib/packlist/model.rb +178 -0
- data/lib/packlist/report.rb +18 -0
- data/lib/packlist/template.rb +55 -0
- data/lib/packlist/utils.rb +20 -0
- data/lib/packlist/version.rb +3 -0
- data/packlist.gemspec +31 -0
- data/test.html +81 -0
- metadata +182 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Packlist - <%= packlist.name %></title>
|
5
|
+
<style type="text/css">
|
6
|
+
<%= partial '../stylesheets/system-font' %>
|
7
|
+
<%= partial '../stylesheets/report' %>
|
8
|
+
</style>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none">
|
12
|
+
<%= partial "../images/worn.svg" %>
|
13
|
+
<%= partial "../images/consumed.svg" %>
|
14
|
+
</svg>
|
15
|
+
|
16
|
+
<header>
|
17
|
+
<h1><%= packlist.name %></h1>
|
18
|
+
<p><%= packlist.description %></p>
|
19
|
+
</header>
|
20
|
+
|
21
|
+
<section>
|
22
|
+
<div class="summary">
|
23
|
+
<table>
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<th>Category</th>
|
27
|
+
<th>Weight</th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tfoot>
|
31
|
+
<tr>
|
32
|
+
<td>Base</td>
|
33
|
+
<td class="weight"><%= packlist.base_weight.lb %></td>
|
34
|
+
</tr>
|
35
|
+
<tr>
|
36
|
+
<td>Consumable</td>
|
37
|
+
<td class="weight"><%= packlist.consumable_weight.lb %></td>
|
38
|
+
</tr>
|
39
|
+
<tr>
|
40
|
+
<td>Worn</td>
|
41
|
+
<td class="weight"><%= packlist.worn_weight.lb %></td>
|
42
|
+
</tr>
|
43
|
+
<tr>
|
44
|
+
<td>Max Pack Weight</td>
|
45
|
+
<td class="weight"><%= packlist.total_pack_weight.lb %></td>
|
46
|
+
</tr>
|
47
|
+
<tr>
|
48
|
+
<td>Total</td>
|
49
|
+
<td class="weight"><%= packlist.total_weight.lb %></td>
|
50
|
+
</tr>
|
51
|
+
</tfoot>
|
52
|
+
<tbody>
|
53
|
+
<% for category in packlist %>
|
54
|
+
<tr>
|
55
|
+
<td><%= category.name %></td>
|
56
|
+
<td><%= category.total_weight.lb %></td>
|
57
|
+
</tr>
|
58
|
+
<% end %>
|
59
|
+
</tbody>
|
60
|
+
</table>
|
61
|
+
</div>
|
62
|
+
|
63
|
+
<div class="categories">
|
64
|
+
<% for category in packlist %>
|
65
|
+
<div class="category">
|
66
|
+
<h2><%= category.name %></h2>
|
67
|
+
|
68
|
+
<table>
|
69
|
+
<tfoot>
|
70
|
+
<tr>
|
71
|
+
<td colspan="2"></td>
|
72
|
+
<td class="subtotal" colspan="3">Subtotal</td>
|
73
|
+
<td class="numeric">
|
74
|
+
<%= category.total_weight.g %>
|
75
|
+
</td>
|
76
|
+
<td class="numeric">
|
77
|
+
<%= category.total_weight.lb %>
|
78
|
+
</td>
|
79
|
+
</tr>
|
80
|
+
</tfoot>
|
81
|
+
<tbody>
|
82
|
+
<% for item in category %>
|
83
|
+
<tr>
|
84
|
+
<td>
|
85
|
+
<input type="checkbox">
|
86
|
+
</td>
|
87
|
+
<td class="item-type">
|
88
|
+
<% if item.worn? %>
|
89
|
+
<svg class="icon worn">
|
90
|
+
<use xlink:href="#worn" />
|
91
|
+
</svg>
|
92
|
+
<% elsif item.consumable? %>
|
93
|
+
<svg class="icon consumable">
|
94
|
+
<use xlink:href="#consumed" />
|
95
|
+
</svg>
|
96
|
+
<% end %>
|
97
|
+
</td>
|
98
|
+
<td class="item-name">
|
99
|
+
<%= item.name %>
|
100
|
+
</td>
|
101
|
+
<td class="item-description">
|
102
|
+
<%= item.description %>
|
103
|
+
</td>
|
104
|
+
<td class="quantity">
|
105
|
+
<% if item.quantity != 1 %>
|
106
|
+
<%= item.quantity %>
|
107
|
+
<% end %>
|
108
|
+
</td>
|
109
|
+
<td class="item-weight">
|
110
|
+
<%= item.total_weight.g %>
|
111
|
+
</td>
|
112
|
+
<td class="item-weight">
|
113
|
+
<%= item.total_weight.oz %>
|
114
|
+
</td>
|
115
|
+
</tr>
|
116
|
+
<% end %>
|
117
|
+
</tbody>
|
118
|
+
</table>
|
119
|
+
</div>
|
120
|
+
<% end %>
|
121
|
+
</div>
|
122
|
+
</section>
|
123
|
+
</body>
|
124
|
+
</html>
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "packlist"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start
|
data/bin/setup
ADDED
Binary file
|
data/docs/sample_pack.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
packlist do
|
2
|
+
name "Epic Backpacking Trip"
|
3
|
+
description "Seven nights in the Sierras, September 2016"
|
4
|
+
|
5
|
+
category "Shelter and sleeping" do
|
6
|
+
# Weight for this item is 280 grams (280.g)
|
7
|
+
item "Tarp", "2-person tarp", 280.g
|
8
|
+
|
9
|
+
# Item quantity can be specified. Here, the weight is per
|
10
|
+
# item, not the total weight of all items.
|
11
|
+
# Here, we specify 8 tent stakes, at 14 grams each.
|
12
|
+
item "Tent stakes", "Aluminum Y-stakes", 14.g, 8
|
13
|
+
|
14
|
+
# Other units of weight can be used as well.
|
15
|
+
# g - grams
|
16
|
+
# kg - kilograms
|
17
|
+
# oz - ounces
|
18
|
+
# lb - pounds
|
19
|
+
# Weight for this item is 24 ounces (24.oz)
|
20
|
+
item "Sleeping back", "Down 20 degree bag", 24.oz
|
21
|
+
|
22
|
+
# Leaving the heavy 4-season tent behind, so we comment it out
|
23
|
+
# item "Tent", "4-season, 3-person tent", 8.5.lb
|
24
|
+
end
|
25
|
+
|
26
|
+
category "Clothing" do
|
27
|
+
# On a backpacking trip, you're likely to wear some of your
|
28
|
+
# clothes (we hope!), while others, like rain gear and extra socks
|
29
|
+
# will be carried most of the time in your pack.
|
30
|
+
worn "T-shirt", "Wool shirt", 150.g
|
31
|
+
worn "Sock", "Wool socks", 80.g
|
32
|
+
worn "Shorts", "Running shorts", 90.g
|
33
|
+
|
34
|
+
# Some things are better left without a description. Just use an
|
35
|
+
# empty string ("").
|
36
|
+
worn "Underwear", "", 85.g
|
37
|
+
|
38
|
+
# The following items are carried in the pack, not worn.
|
39
|
+
item "Rain Jacket", "Keepin' high and dry!", 200.g
|
40
|
+
item "Insulated Jacket", "Down duffy jacket", 275.g
|
41
|
+
item "Warm hat", "Fleece beanie", 90.g
|
42
|
+
end
|
43
|
+
|
44
|
+
# Group your pack's items into multiple categories.
|
45
|
+
category "Cooking" do
|
46
|
+
item "Stove", "Canister stove", 90.g
|
47
|
+
item "Cooking pot", "900ml Titanium pot", 125.g
|
48
|
+
|
49
|
+
# Items like fuel are consumable. They are included in your
|
50
|
+
# maximum pack weight, but not in your base weight.
|
51
|
+
consumable "Fuel", "All season fuel blend", 110.g
|
52
|
+
|
53
|
+
# The canister the fuel comes in is not consumable; to accurately
|
54
|
+
# calculate your pack weight, you should list it separately.
|
55
|
+
item "Fuel canister", "4 ounce fuel canister, empty weight", 90.g
|
56
|
+
end
|
57
|
+
|
58
|
+
category "Food" do
|
59
|
+
# Food is also consumable, of course. You can use either the
|
60
|
+
# "food" or "consumable" item type; the result is the same.
|
61
|
+
|
62
|
+
# For each meal (breakfast, snacks, dinner), we specify the weight
|
63
|
+
# per day, with a quantity of 7, one for each day.
|
64
|
+
food "Breakfast", "Oatmeal", 150.g, 7
|
65
|
+
food "Snacks", "G.O.R.P", 300.g, 7
|
66
|
+
food "Dinner", "Dehydrated gruel", 180.g, 7
|
67
|
+
end
|
68
|
+
end
|
data/exe/packlist
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'packlist'
|
4
|
+
|
5
|
+
def output_filename(filename)
|
6
|
+
basename = File.basename(filename, ".*")
|
7
|
+
dirname = File.dirname(filename)
|
8
|
+
File.join(dirname, "#{basename}.html")
|
9
|
+
end
|
10
|
+
|
11
|
+
ARGV.each do |file|
|
12
|
+
pack = PackList::DSL.load(file)
|
13
|
+
report = PackList::Report.new pack
|
14
|
+
|
15
|
+
report.save_to(output_filename(file))
|
16
|
+
end
|
data/lib/packlist.rb
ADDED
data/lib/packlist/dsl.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'packlist/model'
|
2
|
+
|
3
|
+
module PackList
|
4
|
+
module DSL
|
5
|
+
def packlist(&block)
|
6
|
+
builder = PackBuilder.new &block
|
7
|
+
builder.build
|
8
|
+
end
|
9
|
+
module_function :packlist
|
10
|
+
|
11
|
+
class PackBuilder
|
12
|
+
def initialize(&block)
|
13
|
+
@categories = []
|
14
|
+
if block_given?
|
15
|
+
instance_eval &block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def name(name)
|
20
|
+
@name = name
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def description(description)
|
25
|
+
@description = description
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def build
|
30
|
+
PackList.new @name, @description, @categories
|
31
|
+
end
|
32
|
+
|
33
|
+
def category(name, description=nil, &block)
|
34
|
+
cat_builder = CategoryBuilder.new name, description, &block
|
35
|
+
category = cat_builder.build
|
36
|
+
@categories << category
|
37
|
+
category
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
class CategoryBuilder
|
43
|
+
def initialize(name, description, &block)
|
44
|
+
@category = Category.new name, description
|
45
|
+
if block_given?
|
46
|
+
instance_eval(&block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def item(name, description, weight, quantity=1)
|
51
|
+
add_item(name, description, weight, :item, quantity)
|
52
|
+
end
|
53
|
+
|
54
|
+
def worn(name, description, weight, quantity=1)
|
55
|
+
add_item(name, description, weight, :worn, quantity)
|
56
|
+
end
|
57
|
+
|
58
|
+
def consumable(name, description, weight=:oz, quantity=1)
|
59
|
+
add_item(name, description, weight, :consumable, quantity)
|
60
|
+
end
|
61
|
+
|
62
|
+
alias_method :food, :consumable
|
63
|
+
|
64
|
+
def build
|
65
|
+
@category
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def add_item(name, description, weight, type, quantity)
|
71
|
+
item = Item.new(name, description, weight, type, quantity: quantity)
|
72
|
+
@category << item
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.load(filename)
|
78
|
+
instance_eval(File.read(filename, encoding: "UTF-8"), filename)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module PackList
|
4
|
+
module WeightMixin
|
5
|
+
[:oz, :lb, :g, :kg].each do |units|
|
6
|
+
send :define_method, units do
|
7
|
+
return Weight.new self, units
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Weight
|
13
|
+
WEIGHT_CONVERSIONS = {lb: 453.592, oz: 453.592 / 16, kg: 1000, g: 1}
|
14
|
+
attr_reader :quantity, :units
|
15
|
+
|
16
|
+
def initialize(quantity, units)
|
17
|
+
raise ArgumentError, "Invalid units: #{units}" unless WEIGHT_CONVERSIONS.key?(units)
|
18
|
+
@quantity, @units = quantity, units
|
19
|
+
end
|
20
|
+
|
21
|
+
[:oz, :lb, :g, :kg].each do |units|
|
22
|
+
send :define_method, units do
|
23
|
+
to_units units
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def +(w)
|
28
|
+
# avoid changing units when either weight is zero
|
29
|
+
case
|
30
|
+
when w.quantity == 0
|
31
|
+
self
|
32
|
+
when self.quantity == 0
|
33
|
+
w
|
34
|
+
when w.units == @units
|
35
|
+
Weight.new w.quantity + @quantity, @units
|
36
|
+
else
|
37
|
+
self + w.to_units(@units)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def *(x)
|
42
|
+
Weight.new x * @quantity, @units
|
43
|
+
end
|
44
|
+
|
45
|
+
ZERO = Weight.new(0, :g)
|
46
|
+
|
47
|
+
# private
|
48
|
+
def in_units(new_units)
|
49
|
+
quantity * WEIGHT_CONVERSIONS[units] / WEIGHT_CONVERSIONS[new_units]
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_units(new_units)
|
53
|
+
Weight.new in_units(new_units), new_units
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
"#{'%.1f' % quantity} #{units}"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class Item
|
64
|
+
attr_accessor :name, :description, :weight, :quantity
|
65
|
+
|
66
|
+
def initialize(name, description, weight, type=nil, quantity: 1)
|
67
|
+
@name, @description, @quantity, @type = name, description, quantity, type
|
68
|
+
@weight = weight.respond_to?(:units) ? weight : Weight.new(weight, :oz)
|
69
|
+
end
|
70
|
+
|
71
|
+
def total_weight
|
72
|
+
return @weight * @quantity
|
73
|
+
end
|
74
|
+
|
75
|
+
def worn?
|
76
|
+
return @type == :worn
|
77
|
+
end
|
78
|
+
|
79
|
+
def consumable?
|
80
|
+
return @type == :consumable
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class Category
|
86
|
+
include Enumerable
|
87
|
+
extend Forwardable
|
88
|
+
|
89
|
+
attr_accessor :name, :description
|
90
|
+
attr_reader :items
|
91
|
+
def_delegators :@items, :each, :<<, :include?, :empty?
|
92
|
+
|
93
|
+
def initialize(name, description=nil)
|
94
|
+
@name, @description = name, description
|
95
|
+
@items = []
|
96
|
+
end
|
97
|
+
|
98
|
+
def total_weight
|
99
|
+
@items.collect {|item| item.total_weight}.reduce(Weight::ZERO, :+)
|
100
|
+
end
|
101
|
+
|
102
|
+
def total_pack_weight
|
103
|
+
base_weight + consumable_weight
|
104
|
+
end
|
105
|
+
|
106
|
+
def worn_weight
|
107
|
+
items = @items.select {|item| item.worn? }
|
108
|
+
items.collect {|item| item.total_weight}.reduce(Weight::ZERO, :+)
|
109
|
+
end
|
110
|
+
|
111
|
+
def consumable_weight
|
112
|
+
items = @items.select {|item| item.consumable? }
|
113
|
+
items.collect {|item| item.total_weight}.reduce(Weight::ZERO, :+)
|
114
|
+
end
|
115
|
+
|
116
|
+
def base_weight
|
117
|
+
items = @items.reject {|item| item.worn? || item.consumable? }
|
118
|
+
items.collect {|item| item.total_weight}.reduce(Weight::ZERO, :+)
|
119
|
+
end
|
120
|
+
|
121
|
+
def total_quantity
|
122
|
+
@items.collect {|item| item.quantity}.reduce(0, :+)
|
123
|
+
end
|
124
|
+
|
125
|
+
def item_count
|
126
|
+
@items.length
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
class PackList
|
132
|
+
include Enumerable
|
133
|
+
extend Forwardable
|
134
|
+
|
135
|
+
attr_accessor :name, :description
|
136
|
+
attr_reader :categories
|
137
|
+
def_delegators :@categories, :each, :<<, :include?, :empty?
|
138
|
+
|
139
|
+
def initialize(name, description=nil, categories=[])
|
140
|
+
@name, @description = name, description
|
141
|
+
|
142
|
+
@categories = categories
|
143
|
+
end
|
144
|
+
|
145
|
+
def total_weight
|
146
|
+
@categories.collect {|category| category.total_weight}.reduce(Weight::ZERO, :+)
|
147
|
+
end
|
148
|
+
|
149
|
+
def total_pack_weight
|
150
|
+
base_weight + consumable_weight
|
151
|
+
end
|
152
|
+
|
153
|
+
def worn_weight
|
154
|
+
@categories.collect {|category| category.worn_weight}.reduce(Weight::ZERO, :+)
|
155
|
+
end
|
156
|
+
|
157
|
+
def consumable_weight
|
158
|
+
@categories.collect {|category| category.consumable_weight}.reduce(Weight::ZERO, :+)
|
159
|
+
end
|
160
|
+
|
161
|
+
def base_weight
|
162
|
+
@categories.collect {|category| category.base_weight}.reduce(Weight::ZERO, :+)
|
163
|
+
end
|
164
|
+
|
165
|
+
def total_quantity
|
166
|
+
@categories.collect {|category| category.total_quantity}.reduce(0, :+)
|
167
|
+
end
|
168
|
+
|
169
|
+
def item_count
|
170
|
+
@categories.collect {|category| category.item_count}.reduce(0, :+)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class Numeric
|
177
|
+
include PackList::WeightMixin
|
178
|
+
end
|