yarn_skein 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/yarn_skein/catalog.rb +73 -0
- data/lib/yarn_skein/colorwork_estimator.rb +121 -0
- data/lib/yarn_skein/version.rb +1 -1
- data/lib/yarn_skein/yardage_estimator.rb +71 -0
- data/lib/yarn_skein.rb +4 -0
- data/test/catalog_test.rb +182 -0
- data/test/colorwork_estimator_test.rb +283 -0
- data/test/yardage_estimator_test.rb +170 -0
- metadata +25 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e58123240286568c5799c6ee0d7ed8572de554f44c4e2cbf653e178be29a181
|
|
4
|
+
data.tar.gz: bd9aba63f86efb708f7156575d7136d91cd660f05bc857b5a0cb50b8087586b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a13f3cde5def54d9bdd85026f2d143a9e3d0e9ca4b76023573839c8fc6f6d1b224580e62eb19907b6559a08391103470efb71fb7e6dba428d1364f64d4ea165
|
|
7
|
+
data.tar.gz: 398c7cfaa30a61beb4839fda0bc96737023b03df676b1fb85cae9e8854e4d7119e2f32be1de5fe2a86d402ea365450a0547b8749938395f68cb18644b61d5567
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `YarnSkein::ColorworkEstimator` for estimating per-color yarn yardage in stranded and intarsia colorwork
|
|
7
|
+
- Supports float overhead calculation for stranded colorwork (default 20%)
|
|
8
|
+
- Per-color breakdown with optional skein calculations when yarn is provided
|
|
9
|
+
- Configurable safety margin (default 10%) consistent with `YardageEstimator`
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-03-26
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `YarnSkein::Catalog` for loading yarn data from YAML seed files with filtering by brand, weight category, and fiber content
|
|
15
|
+
- `YarnSkein::YardageEstimator` for estimating total yarn yardage from gauge and dimensions
|
|
16
|
+
- Rectangular piece estimation via `for_rectangle(width:, height:)`
|
|
17
|
+
- Stitch/row count estimation via `for_piece(stitches:, rows:)`
|
|
18
|
+
- Configurable safety margin (default 10%) for waste and joining
|
|
19
|
+
- Added `fiber_gauge` as a runtime dependency
|
|
3
20
|
|
|
4
21
|
## [0.2.0] - 2026-03-21
|
|
5
22
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module YarnSkein
|
|
6
|
+
class Catalog
|
|
7
|
+
def initialize(data_dir: nil)
|
|
8
|
+
@data_dir = data_dir || default_data_dir
|
|
9
|
+
@yarns = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def all
|
|
13
|
+
load_yarns
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def filter(brand: nil, weight_category: nil, fiber: nil)
|
|
17
|
+
results = all
|
|
18
|
+
results = results.select { |y| y.brand.downcase == brand.downcase } if brand
|
|
19
|
+
results = results.select { |y| y.weight_category == weight_category.to_sym } if weight_category
|
|
20
|
+
results = results.select { |y| y.fiber_content&.contains?(fiber.to_sym) } if fiber
|
|
21
|
+
results
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def brands
|
|
25
|
+
all.map(&:brand).uniq.sort
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find(brand:, line:)
|
|
29
|
+
all.detect { |y| y.brand.downcase == brand.downcase && y.line.downcase == line.downcase }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def load_yarns
|
|
35
|
+
@yarns ||= Dir.glob(File.join(@data_dir, "*.yml")).reject { |f| File.basename(f).start_with?("_") }.flat_map { |file| parse_brand_file(file) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_brand_file(file)
|
|
39
|
+
data = YAML.safe_load_file(file, permitted_classes: [])
|
|
40
|
+
brand_name = data["brand"]
|
|
41
|
+
(data["yarns"] || []).filter_map do |entry|
|
|
42
|
+
build_yarn(brand_name, entry)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_yarn(brand_name, entry)
|
|
47
|
+
yardage = entry["yardage"]
|
|
48
|
+
skein_weight = entry["skein_weight"]
|
|
49
|
+
return nil unless yardage && skein_weight
|
|
50
|
+
|
|
51
|
+
fiber_content = nil
|
|
52
|
+
if entry["fiber_content"] && !entry["fiber_content"].empty?
|
|
53
|
+
fiber_hash = entry["fiber_content"].transform_keys(&:to_sym).transform_values(&:to_i)
|
|
54
|
+
fiber_content = FiberBlend.new(fiber_hash)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Yarn.new(
|
|
58
|
+
brand: brand_name,
|
|
59
|
+
line: entry["line"],
|
|
60
|
+
yardage: yardage.to_f.yards,
|
|
61
|
+
skein_weight: skein_weight.to_f.grams,
|
|
62
|
+
fiber_content: fiber_content
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_data_dir
|
|
67
|
+
env_path = ENV["YARN_CATALOG_PATH"]
|
|
68
|
+
return env_path if env_path
|
|
69
|
+
|
|
70
|
+
File.expand_path("../../../../data/yarns", __dir__)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YarnSkein
|
|
4
|
+
# Estimates per-color yarn yardage for colorwork techniques.
|
|
5
|
+
#
|
|
6
|
+
# Supports stranded (Fair Isle) and intarsia colorwork. Stranded colorwork
|
|
7
|
+
# adds a float overhead factor because unused colors are carried across the
|
|
8
|
+
# back of the fabric. Intarsia uses separate yarn sections with no floats.
|
|
9
|
+
#
|
|
10
|
+
# @example Stranded colorwork
|
|
11
|
+
# estimator = YarnSkein::ColorworkEstimator.new(
|
|
12
|
+
# gauge: gauge,
|
|
13
|
+
# technique: :stranded
|
|
14
|
+
# )
|
|
15
|
+
# estimator.estimate(
|
|
16
|
+
# width: 40.inches, height: 24.inches,
|
|
17
|
+
# colors: { main: 0.60, contrast: 0.40 },
|
|
18
|
+
# yarn: yarn
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Intarsia colorwork
|
|
22
|
+
# estimator = YarnSkein::ColorworkEstimator.new(
|
|
23
|
+
# gauge: gauge,
|
|
24
|
+
# technique: :intarsia
|
|
25
|
+
# )
|
|
26
|
+
# estimator.estimate(
|
|
27
|
+
# width: 40.inches, height: 24.inches,
|
|
28
|
+
# colors: { left: 0.50, right: 0.50 },
|
|
29
|
+
# yarn: yarn
|
|
30
|
+
# )
|
|
31
|
+
class ColorworkEstimator
|
|
32
|
+
TECHNIQUES = %i[stranded intarsia].freeze
|
|
33
|
+
|
|
34
|
+
# Default float overhead for stranded colorwork (20%).
|
|
35
|
+
DEFAULT_FLOAT_OVERHEAD = 0.20
|
|
36
|
+
|
|
37
|
+
# Default safety margin (10%), consistent with YardageEstimator.
|
|
38
|
+
DEFAULT_MARGIN = 0.10
|
|
39
|
+
|
|
40
|
+
# @return [FiberGauge::Gauge]
|
|
41
|
+
attr_reader :gauge
|
|
42
|
+
|
|
43
|
+
# @return [Symbol] :stranded or :intarsia
|
|
44
|
+
attr_reader :technique
|
|
45
|
+
|
|
46
|
+
# @param gauge [FiberGauge::Gauge] gauge swatch data
|
|
47
|
+
# @param technique [Symbol] :stranded or :intarsia
|
|
48
|
+
def initialize(gauge:, technique:)
|
|
49
|
+
unless TECHNIQUES.include?(technique)
|
|
50
|
+
raise ArgumentError, "technique must be one of: #{TECHNIQUES.join(", ")}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@gauge = gauge
|
|
54
|
+
@technique = technique
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Estimates per-color yardage for a colorwork piece.
|
|
58
|
+
#
|
|
59
|
+
# @param width [FiberUnits::Length] finished width
|
|
60
|
+
# @param height [FiberUnits::Length] finished height
|
|
61
|
+
# @param colors [Hash<Symbol, Float>] color name => proportion (must sum to 1.0)
|
|
62
|
+
# @param yarn [YarnSkein::Yarn, nil] yarn for skein calculations
|
|
63
|
+
# @param margin [Float] safety margin (default 10%)
|
|
64
|
+
# @param float_overhead [Float] extra yarn for floats in stranded work (default 20%)
|
|
65
|
+
# @return [Hash] per-color breakdown and total
|
|
66
|
+
def estimate(width:, height:, colors:, yarn: nil, margin: DEFAULT_MARGIN, float_overhead: DEFAULT_FLOAT_OVERHEAD)
|
|
67
|
+
validate_colors!(colors)
|
|
68
|
+
|
|
69
|
+
base_yardage = base_yardage_for(width, height, margin: 0.0)
|
|
70
|
+
result = {}
|
|
71
|
+
total_yards = 0.0
|
|
72
|
+
|
|
73
|
+
colors.each do |color_name, proportion|
|
|
74
|
+
color_yards = base_yardage * proportion
|
|
75
|
+
color_yards *= (1.0 + float_overhead) if technique == :stranded
|
|
76
|
+
color_yards *= (1.0 + margin)
|
|
77
|
+
total_yards += color_yards
|
|
78
|
+
|
|
79
|
+
entry = {yardage: color_yards.yards}
|
|
80
|
+
entry[:skeins] = yarn.skeins_required(color_yards.yards) if yarn
|
|
81
|
+
result[color_name] = entry
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
result[:total] = {yardage: total_yards.yards}
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Compute raw yardage (in float yards) for a rectangle with no margin.
|
|
91
|
+
def base_yardage_for(width, height, margin:)
|
|
92
|
+
stitches = gauge.required_stitches(width)
|
|
93
|
+
rows = gauge.required_rows(height)
|
|
94
|
+
total_stitches = stitches.value * rows.value
|
|
95
|
+
|
|
96
|
+
total_stitches * yards_per_stitch
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Yards of yarn consumed per stitch, using the geometric U-loop model.
|
|
100
|
+
def yards_per_stitch
|
|
101
|
+
stitch_width_in = 1.0 / gauge.spi
|
|
102
|
+
stitch_height_in = 1.0 / gauge.rpi
|
|
103
|
+
(stitch_width_in + (2.0 * stitch_height_in)) / 36.0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_colors!(colors)
|
|
107
|
+
raise ArgumentError, "colors must be a non-empty Hash" unless colors.is_a?(Hash) && !colors.empty?
|
|
108
|
+
|
|
109
|
+
total = colors.values.sum
|
|
110
|
+
unless (total - 1.0).abs < 0.001
|
|
111
|
+
raise ArgumentError, "color proportions must sum to 1.0 (got #{total})"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
colors.each do |name, proportion|
|
|
115
|
+
unless proportion.is_a?(Numeric) && proportion > 0
|
|
116
|
+
raise ArgumentError, "proportion for #{name} must be a positive number"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/yarn_skein/version.rb
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YarnSkein
|
|
4
|
+
# Estimates total yarn yardage required for a project based on gauge and yarn.
|
|
5
|
+
#
|
|
6
|
+
# Uses a geometric stitch model: each knit stitch consumes approximately
|
|
7
|
+
# one stitch-width plus two stitch-heights of yarn (the U-shaped loop path).
|
|
8
|
+
# This gives a reasonable estimate for stockinette and similar stitch patterns.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# estimator = YarnSkein::YardageEstimator.new(gauge: gauge, yarn: yarn)
|
|
12
|
+
# estimator.for_rectangle(width: 60.inches, height: 72.inches)
|
|
13
|
+
# # => { yardage: <FiberUnits::Length>, skeins: 10 }
|
|
14
|
+
class YardageEstimator
|
|
15
|
+
# Default safety margin added to yardage estimates (10%).
|
|
16
|
+
DEFAULT_MARGIN = 0.10
|
|
17
|
+
|
|
18
|
+
# @return [FiberGauge::Gauge] gauge used for stitch/row calculations
|
|
19
|
+
attr_reader :gauge
|
|
20
|
+
|
|
21
|
+
# @return [YarnSkein::Yarn] yarn used for skein calculations
|
|
22
|
+
attr_reader :yarn
|
|
23
|
+
|
|
24
|
+
# @param gauge [FiberGauge::Gauge] gauge swatch data
|
|
25
|
+
# @param yarn [YarnSkein::Yarn] yarn being used
|
|
26
|
+
def initialize(gauge:, yarn:)
|
|
27
|
+
@gauge = gauge
|
|
28
|
+
@yarn = yarn
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Estimates yardage for a rectangular piece (scarf, blanket, etc.).
|
|
32
|
+
#
|
|
33
|
+
# @param width [FiberUnits::Length] finished width
|
|
34
|
+
# @param height [FiberUnits::Length] finished height
|
|
35
|
+
# @param margin [Float] safety margin as a decimal (default 0.10 = 10%)
|
|
36
|
+
# @return [Hash] :yardage [FiberUnits::Length] and :skeins [Integer]
|
|
37
|
+
def for_rectangle(width:, height:, margin: DEFAULT_MARGIN)
|
|
38
|
+
stitches = gauge.required_stitches(width)
|
|
39
|
+
rows = gauge.required_rows(height)
|
|
40
|
+
|
|
41
|
+
for_piece(stitches: stitches, rows: rows, margin: margin)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Estimates yardage from stitch and row counts directly.
|
|
45
|
+
#
|
|
46
|
+
# @param stitches [FiberUnits::StitchCount] total stitches across
|
|
47
|
+
# @param rows [FiberUnits::RowCount] total rows
|
|
48
|
+
# @param margin [Float] safety margin as a decimal (default 0.10 = 10%)
|
|
49
|
+
# @return [Hash] :yardage [FiberUnits::Length] and :skeins [Integer]
|
|
50
|
+
def for_piece(stitches:, rows:, margin: DEFAULT_MARGIN)
|
|
51
|
+
total_stitches = stitches.value * rows.value
|
|
52
|
+
yardage = (total_stitches * yards_per_stitch * (1.0 + margin)).yards
|
|
53
|
+
|
|
54
|
+
{yardage: yardage, skeins: yarn.skeins_required(yardage)}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the estimated yards of yarn consumed per stitch.
|
|
58
|
+
#
|
|
59
|
+
# Models each stitch as a U-shaped loop: one stitch-width across plus
|
|
60
|
+
# two stitch-heights for the legs of the loop.
|
|
61
|
+
#
|
|
62
|
+
# @return [Float] yards per stitch
|
|
63
|
+
def yards_per_stitch
|
|
64
|
+
stitch_width_in = 1.0 / gauge.spi
|
|
65
|
+
stitch_height_in = 1.0 / gauge.rpi
|
|
66
|
+
yarn_per_stitch_in = stitch_width_in + (2.0 * stitch_height_in)
|
|
67
|
+
|
|
68
|
+
yarn_per_stitch_in / 36.0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/yarn_skein.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fiber_units"
|
|
4
|
+
require "fiber_gauge"
|
|
4
5
|
require_relative "yarn_skein/version"
|
|
5
6
|
require_relative "yarn_skein/weight_category"
|
|
6
7
|
require_relative "yarn_skein/fiber_blend"
|
|
7
8
|
require_relative "yarn_skein/yarn"
|
|
8
9
|
require_relative "yarn_skein/substitution"
|
|
10
|
+
require_relative "yarn_skein/yardage_estimator"
|
|
11
|
+
require_relative "yarn_skein/colorwork_estimator"
|
|
12
|
+
require_relative "yarn_skein/catalog"
|
|
9
13
|
|
|
10
14
|
# Domain objects for describing yarn skeins, fiber blends, and weight classes.
|
|
11
15
|
#
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
class CatalogTest < Minitest::Test
|
|
9
|
+
def setup
|
|
10
|
+
@data_dir = Dir.mktmpdir("catalog_test")
|
|
11
|
+
write_fixture("malabrigo.yml", {
|
|
12
|
+
"brand" => "Malabrigo",
|
|
13
|
+
"slug" => "malabrigo",
|
|
14
|
+
"yarns" => [
|
|
15
|
+
{
|
|
16
|
+
"line" => "Rios",
|
|
17
|
+
"slug" => "rios",
|
|
18
|
+
"weight_category" => "worsted",
|
|
19
|
+
"yardage" => 210,
|
|
20
|
+
"skein_weight" => 100,
|
|
21
|
+
"fiber_content" => {"wool" => 100},
|
|
22
|
+
"colorways" => []
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"line" => "Mechita",
|
|
26
|
+
"slug" => "mechita",
|
|
27
|
+
"weight_category" => "fingering",
|
|
28
|
+
"yardage" => 420,
|
|
29
|
+
"skein_weight" => 100,
|
|
30
|
+
"fiber_content" => {"wool" => 100},
|
|
31
|
+
"colorways" => []
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
write_fixture("cascade.yml", {
|
|
37
|
+
"brand" => "Cascade",
|
|
38
|
+
"slug" => "cascade",
|
|
39
|
+
"yarns" => [
|
|
40
|
+
{
|
|
41
|
+
"line" => "220 Superwash",
|
|
42
|
+
"slug" => "220-superwash",
|
|
43
|
+
"weight_category" => "worsted",
|
|
44
|
+
"yardage" => 200,
|
|
45
|
+
"skein_weight" => 100,
|
|
46
|
+
"fiber_content" => {"wool" => 100},
|
|
47
|
+
"colorways" => []
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
# Meta file should be ignored
|
|
53
|
+
write_fixture("_meta.yml", {"scraped_at" => "2026-01-01", "version" => "1.0"})
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def teardown
|
|
57
|
+
FileUtils.rm_rf(@data_dir)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ---- all ----
|
|
61
|
+
|
|
62
|
+
def test_all_returns_yarn_objects
|
|
63
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
64
|
+
yarns = catalog.all
|
|
65
|
+
|
|
66
|
+
assert_equal 3, yarns.size
|
|
67
|
+
assert yarns.all? { |y| y.is_a?(YarnSkein::Yarn) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_all_skips_meta_files
|
|
71
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
72
|
+
brands = catalog.all.map(&:brand).uniq
|
|
73
|
+
|
|
74
|
+
refute_includes brands, "scraped_at"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ---- filter ----
|
|
78
|
+
|
|
79
|
+
def test_filter_by_brand
|
|
80
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
81
|
+
results = catalog.filter(brand: "Malabrigo")
|
|
82
|
+
|
|
83
|
+
assert_equal 2, results.size
|
|
84
|
+
assert results.all? { |y| y.brand == "Malabrigo" }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_filter_by_weight_category
|
|
88
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
89
|
+
results = catalog.filter(weight_category: :worsted)
|
|
90
|
+
|
|
91
|
+
assert_equal 2, results.size
|
|
92
|
+
assert results.all? { |y| y.weight_category == :worsted }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_filter_by_fiber
|
|
96
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
97
|
+
results = catalog.filter(fiber: :wool)
|
|
98
|
+
|
|
99
|
+
assert_equal 3, results.size
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_filter_combined
|
|
103
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
104
|
+
results = catalog.filter(brand: "Malabrigo", weight_category: :worsted)
|
|
105
|
+
|
|
106
|
+
assert_equal 1, results.size
|
|
107
|
+
assert_equal "Rios", results.first.line
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ---- brands ----
|
|
111
|
+
|
|
112
|
+
def test_brands_returns_unique_sorted_list
|
|
113
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
114
|
+
|
|
115
|
+
assert_equal ["Cascade", "Malabrigo"], catalog.brands
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ---- find ----
|
|
119
|
+
|
|
120
|
+
def test_find_returns_matching_yarn
|
|
121
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
122
|
+
yarn = catalog.find(brand: "Malabrigo", line: "Rios")
|
|
123
|
+
|
|
124
|
+
assert_equal "Malabrigo", yarn.brand
|
|
125
|
+
assert_equal "Rios", yarn.line
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_find_is_case_insensitive
|
|
129
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
130
|
+
yarn = catalog.find(brand: "malabrigo", line: "rios")
|
|
131
|
+
|
|
132
|
+
assert_equal "Malabrigo", yarn.brand
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_find_returns_nil_when_not_found
|
|
136
|
+
catalog = YarnSkein::Catalog.new(data_dir: @data_dir)
|
|
137
|
+
|
|
138
|
+
assert_nil catalog.find(brand: "Unknown", line: "Yarn")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ---- empty directory ----
|
|
142
|
+
|
|
143
|
+
def test_empty_data_dir
|
|
144
|
+
empty_dir = Dir.mktmpdir("empty_catalog")
|
|
145
|
+
catalog = YarnSkein::Catalog.new(data_dir: empty_dir)
|
|
146
|
+
|
|
147
|
+
assert_empty catalog.all
|
|
148
|
+
assert_empty catalog.brands
|
|
149
|
+
ensure
|
|
150
|
+
FileUtils.rm_rf(empty_dir)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# ---- skips entries without yardage ----
|
|
154
|
+
|
|
155
|
+
def test_skips_entries_missing_yardage
|
|
156
|
+
dir = Dir.mktmpdir("incomplete_catalog")
|
|
157
|
+
write_fixture_to(dir, "incomplete.yml", {
|
|
158
|
+
"brand" => "Incomplete",
|
|
159
|
+
"slug" => "incomplete",
|
|
160
|
+
"yarns" => [
|
|
161
|
+
{"line" => "NoYardage", "slug" => "no-yardage", "skein_weight" => 100, "fiber_content" => {}},
|
|
162
|
+
{"line" => "Valid", "slug" => "valid", "yardage" => 200, "skein_weight" => 100, "fiber_content" => {"wool" => 100}}
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
catalog = YarnSkein::Catalog.new(data_dir: dir)
|
|
167
|
+
assert_equal 1, catalog.all.size
|
|
168
|
+
assert_equal "Valid", catalog.all.first.line
|
|
169
|
+
ensure
|
|
170
|
+
FileUtils.rm_rf(dir)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def write_fixture(filename, data)
|
|
176
|
+
write_fixture_to(@data_dir, filename, data)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def write_fixture_to(dir, filename, data)
|
|
180
|
+
File.write(File.join(dir, filename), YAML.dump(data))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinColorworkEstimatorTest < Minitest::Test
|
|
6
|
+
def gauge
|
|
7
|
+
FiberGauge::Gauge.new(
|
|
8
|
+
stitches: 18.stitches,
|
|
9
|
+
rows: 24.rows,
|
|
10
|
+
width: 4.inches,
|
|
11
|
+
height: 4.inches
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def yarn
|
|
16
|
+
YarnSkein::Yarn.new(
|
|
17
|
+
brand: "Malabrigo",
|
|
18
|
+
line: "Rios",
|
|
19
|
+
yardage: 210.yards,
|
|
20
|
+
skein_weight: 100.grams
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stranded_estimator
|
|
25
|
+
YarnSkein::ColorworkEstimator.new(gauge: gauge, technique: :stranded)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def intarsia_estimator
|
|
29
|
+
YarnSkein::ColorworkEstimator.new(gauge: gauge, technique: :intarsia)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ----------------------------
|
|
33
|
+
# initialization
|
|
34
|
+
# ----------------------------
|
|
35
|
+
|
|
36
|
+
def test_rejects_invalid_technique
|
|
37
|
+
assert_raises(ArgumentError) do
|
|
38
|
+
YarnSkein::ColorworkEstimator.new(gauge: gauge, technique: :mosaic)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_accepts_stranded_technique
|
|
43
|
+
est = YarnSkein::ColorworkEstimator.new(gauge: gauge, technique: :stranded)
|
|
44
|
+
assert_equal :stranded, est.technique
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_accepts_intarsia_technique
|
|
48
|
+
est = YarnSkein::ColorworkEstimator.new(gauge: gauge, technique: :intarsia)
|
|
49
|
+
assert_equal :intarsia, est.technique
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ----------------------------
|
|
53
|
+
# color validation
|
|
54
|
+
# ----------------------------
|
|
55
|
+
|
|
56
|
+
def test_rejects_empty_colors
|
|
57
|
+
assert_raises(ArgumentError) do
|
|
58
|
+
stranded_estimator.estimate(width: 20.inches, height: 20.inches, colors: {})
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_rejects_non_hash_colors
|
|
63
|
+
assert_raises(ArgumentError) do
|
|
64
|
+
stranded_estimator.estimate(width: 20.inches, height: 20.inches, colors: [0.5, 0.5])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_rejects_proportions_not_summing_to_one
|
|
69
|
+
assert_raises(ArgumentError) do
|
|
70
|
+
stranded_estimator.estimate(
|
|
71
|
+
width: 20.inches, height: 20.inches,
|
|
72
|
+
colors: {main: 0.60, contrast: 0.30}
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_rejects_zero_proportion
|
|
78
|
+
assert_raises(ArgumentError) do
|
|
79
|
+
stranded_estimator.estimate(
|
|
80
|
+
width: 20.inches, height: 20.inches,
|
|
81
|
+
colors: {main: 1.0, contrast: 0.0}
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ----------------------------
|
|
87
|
+
# stranded estimation
|
|
88
|
+
# ----------------------------
|
|
89
|
+
|
|
90
|
+
def test_stranded_returns_per_color_yardage
|
|
91
|
+
result = stranded_estimator.estimate(
|
|
92
|
+
width: 40.inches, height: 24.inches,
|
|
93
|
+
colors: {main: 0.60, contrast: 0.40}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert result.key?(:main)
|
|
97
|
+
assert result.key?(:contrast)
|
|
98
|
+
assert result.key?(:total)
|
|
99
|
+
assert_instance_of FiberUnits::Length, result[:main][:yardage]
|
|
100
|
+
assert_instance_of FiberUnits::Length, result[:contrast][:yardage]
|
|
101
|
+
assert_instance_of FiberUnits::Length, result[:total][:yardage]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_stranded_includes_float_overhead
|
|
105
|
+
stranded = stranded_estimator.estimate(
|
|
106
|
+
width: 40.inches, height: 24.inches,
|
|
107
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
108
|
+
margin: 0.0, float_overhead: 0.20
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
intarsia = intarsia_estimator.estimate(
|
|
112
|
+
width: 40.inches, height: 24.inches,
|
|
113
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
114
|
+
margin: 0.0
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Stranded total should be 20% more than intarsia (due to float overhead)
|
|
118
|
+
stranded_total = stranded[:total][:yardage].to(:yards).value
|
|
119
|
+
intarsia_total = intarsia[:total][:yardage].to(:yards).value
|
|
120
|
+
|
|
121
|
+
assert_in_delta intarsia_total * 1.20, stranded_total, 0.01
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_stranded_custom_float_overhead
|
|
125
|
+
low_overhead = stranded_estimator.estimate(
|
|
126
|
+
width: 40.inches, height: 24.inches,
|
|
127
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
128
|
+
margin: 0.0, float_overhead: 0.15
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
high_overhead = stranded_estimator.estimate(
|
|
132
|
+
width: 40.inches, height: 24.inches,
|
|
133
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
134
|
+
margin: 0.0, float_overhead: 0.25
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert high_overhead[:total][:yardage].to(:yards).value >
|
|
138
|
+
low_overhead[:total][:yardage].to(:yards).value
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_stranded_proportions_affect_distribution
|
|
142
|
+
result = stranded_estimator.estimate(
|
|
143
|
+
width: 40.inches, height: 24.inches,
|
|
144
|
+
colors: {main: 0.70, contrast: 0.30},
|
|
145
|
+
margin: 0.0
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
main_yards = result[:main][:yardage].to(:yards).value
|
|
149
|
+
contrast_yards = result[:contrast][:yardage].to(:yards).value
|
|
150
|
+
|
|
151
|
+
assert_in_delta 7.0 / 3.0, main_yards / contrast_yards, 0.01
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# ----------------------------
|
|
155
|
+
# intarsia estimation
|
|
156
|
+
# ----------------------------
|
|
157
|
+
|
|
158
|
+
def test_intarsia_no_float_overhead
|
|
159
|
+
intarsia = intarsia_estimator.estimate(
|
|
160
|
+
width: 40.inches, height: 24.inches,
|
|
161
|
+
colors: {left: 0.50, right: 0.50},
|
|
162
|
+
margin: 0.0
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# For intarsia, each half should get half the base yardage
|
|
166
|
+
left_yards = intarsia[:left][:yardage].to(:yards).value
|
|
167
|
+
right_yards = intarsia[:right][:yardage].to(:yards).value
|
|
168
|
+
|
|
169
|
+
assert_in_delta left_yards, right_yards, 0.01
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_intarsia_total_matches_base_yardage
|
|
173
|
+
intarsia = intarsia_estimator.estimate(
|
|
174
|
+
width: 20.inches, height: 30.inches,
|
|
175
|
+
colors: {left: 0.50, right: 0.50},
|
|
176
|
+
margin: 0.0
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Total intarsia yardage should match a plain YardageEstimator (no margin)
|
|
180
|
+
base = YarnSkein::YardageEstimator.new(gauge: gauge, yarn: yarn)
|
|
181
|
+
base_result = base.for_rectangle(width: 20.inches, height: 30.inches, margin: 0.0)
|
|
182
|
+
|
|
183
|
+
assert_in_delta(
|
|
184
|
+
base_result[:yardage].to(:yards).value,
|
|
185
|
+
intarsia[:total][:yardage].to(:yards).value,
|
|
186
|
+
0.01
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# ----------------------------
|
|
191
|
+
# skein calculations
|
|
192
|
+
# ----------------------------
|
|
193
|
+
|
|
194
|
+
def test_includes_skeins_when_yarn_provided
|
|
195
|
+
result = stranded_estimator.estimate(
|
|
196
|
+
width: 40.inches, height: 24.inches,
|
|
197
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
198
|
+
yarn: yarn
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
assert result[:main].key?(:skeins)
|
|
202
|
+
assert result[:contrast].key?(:skeins)
|
|
203
|
+
assert_instance_of Integer, result[:main][:skeins]
|
|
204
|
+
assert_instance_of Integer, result[:contrast][:skeins]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def test_omits_skeins_when_no_yarn
|
|
208
|
+
result = stranded_estimator.estimate(
|
|
209
|
+
width: 40.inches, height: 24.inches,
|
|
210
|
+
colors: {main: 0.60, contrast: 0.40}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
refute result[:main].key?(:skeins)
|
|
214
|
+
refute result[:contrast].key?(:skeins)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# ----------------------------
|
|
218
|
+
# margin
|
|
219
|
+
# ----------------------------
|
|
220
|
+
|
|
221
|
+
def test_default_margin_applied
|
|
222
|
+
no_margin = intarsia_estimator.estimate(
|
|
223
|
+
width: 20.inches, height: 20.inches,
|
|
224
|
+
colors: {main: 1.0},
|
|
225
|
+
margin: 0.0
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
with_margin = intarsia_estimator.estimate(
|
|
229
|
+
width: 20.inches, height: 20.inches,
|
|
230
|
+
colors: {main: 1.0}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert_in_delta(
|
|
234
|
+
no_margin[:main][:yardage].to(:yards).value * 1.10,
|
|
235
|
+
with_margin[:main][:yardage].to(:yards).value,
|
|
236
|
+
0.01
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ----------------------------
|
|
241
|
+
# multi-color
|
|
242
|
+
# ----------------------------
|
|
243
|
+
|
|
244
|
+
def test_three_color_stranded
|
|
245
|
+
result = stranded_estimator.estimate(
|
|
246
|
+
width: 40.inches, height: 24.inches,
|
|
247
|
+
colors: {main: 0.50, contrast_a: 0.30, contrast_b: 0.20},
|
|
248
|
+
margin: 0.0
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert result.key?(:main)
|
|
252
|
+
assert result.key?(:contrast_a)
|
|
253
|
+
assert result.key?(:contrast_b)
|
|
254
|
+
assert result.key?(:total)
|
|
255
|
+
|
|
256
|
+
total = result[:main][:yardage].to(:yards).value +
|
|
257
|
+
result[:contrast_a][:yardage].to(:yards).value +
|
|
258
|
+
result[:contrast_b][:yardage].to(:yards).value
|
|
259
|
+
|
|
260
|
+
assert_in_delta total, result[:total][:yardage].to(:yards).value, 0.01
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# ----------------------------
|
|
264
|
+
# realistic scenario
|
|
265
|
+
# ----------------------------
|
|
266
|
+
|
|
267
|
+
def test_fair_isle_yoke_estimate_is_reasonable
|
|
268
|
+
# A Fair Isle yoke ~20" wide, 12" tall in worsted weight
|
|
269
|
+
result = stranded_estimator.estimate(
|
|
270
|
+
width: 20.inches, height: 12.inches,
|
|
271
|
+
colors: {main: 0.60, contrast: 0.40},
|
|
272
|
+
yarn: yarn
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
main_yards = result[:main][:yardage].to(:yards).value
|
|
276
|
+
contrast_yards = result[:contrast][:yardage].to(:yards).value
|
|
277
|
+
total_yards = result[:total][:yardage].to(:yards).value
|
|
278
|
+
|
|
279
|
+
assert total_yards > 50, "total #{total_yards} seems too low"
|
|
280
|
+
assert total_yards < 500, "total #{total_yards} seems too high"
|
|
281
|
+
assert main_yards > contrast_yards, "main color should use more yarn"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinYardageEstimatorTest < Minitest::Test
|
|
6
|
+
def gauge
|
|
7
|
+
FiberGauge::Gauge.new(
|
|
8
|
+
stitches: 18.stitches,
|
|
9
|
+
rows: 24.rows,
|
|
10
|
+
width: 4.inches,
|
|
11
|
+
height: 4.inches
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def yarn
|
|
16
|
+
YarnSkein::Yarn.new(
|
|
17
|
+
brand: "Malabrigo",
|
|
18
|
+
line: "Rios",
|
|
19
|
+
yardage: 210.yards,
|
|
20
|
+
skein_weight: 100.grams
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def estimator
|
|
25
|
+
YarnSkein::YardageEstimator.new(gauge: gauge, yarn: yarn)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# -----------------------------
|
|
29
|
+
# yards_per_stitch
|
|
30
|
+
# -----------------------------
|
|
31
|
+
|
|
32
|
+
def test_yards_per_stitch_returns_positive_value
|
|
33
|
+
assert estimator.yards_per_stitch > 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_yards_per_stitch_uses_geometric_model
|
|
37
|
+
spi = gauge.spi
|
|
38
|
+
rpi = gauge.rpi
|
|
39
|
+
expected = (1.0 / spi + 2.0 / rpi) / 36.0
|
|
40
|
+
|
|
41
|
+
assert_in_delta expected, estimator.yards_per_stitch, 0.0001
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# -----------------------------
|
|
45
|
+
# for_piece
|
|
46
|
+
# -----------------------------
|
|
47
|
+
|
|
48
|
+
def test_for_piece_returns_yardage_and_skeins
|
|
49
|
+
result = estimator.for_piece(stitches: 100.stitches, rows: 100.rows)
|
|
50
|
+
|
|
51
|
+
assert result.key?(:yardage)
|
|
52
|
+
assert result.key?(:skeins)
|
|
53
|
+
assert_instance_of FiberUnits::Length, result[:yardage]
|
|
54
|
+
assert_instance_of Integer, result[:skeins]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_for_piece_yardage_is_positive
|
|
58
|
+
result = estimator.for_piece(stitches: 100.stitches, rows: 100.rows)
|
|
59
|
+
|
|
60
|
+
assert result[:yardage].value > 0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_for_piece_includes_default_margin
|
|
64
|
+
no_margin = estimator.for_piece(stitches: 100.stitches, rows: 100.rows, margin: 0.0)
|
|
65
|
+
with_margin = estimator.for_piece(stitches: 100.stitches, rows: 100.rows)
|
|
66
|
+
|
|
67
|
+
assert_in_delta(
|
|
68
|
+
no_margin[:yardage].to(:yards).value * 1.10,
|
|
69
|
+
with_margin[:yardage].to(:yards).value,
|
|
70
|
+
0.01
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_for_piece_custom_margin
|
|
75
|
+
no_margin = estimator.for_piece(stitches: 100.stitches, rows: 100.rows, margin: 0.0)
|
|
76
|
+
with_margin = estimator.for_piece(stitches: 100.stitches, rows: 100.rows, margin: 0.20)
|
|
77
|
+
|
|
78
|
+
assert_in_delta(
|
|
79
|
+
no_margin[:yardage].to(:yards).value * 1.20,
|
|
80
|
+
with_margin[:yardage].to(:yards).value,
|
|
81
|
+
0.01
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_for_piece_zero_margin
|
|
86
|
+
result = estimator.for_piece(stitches: 100.stitches, rows: 100.rows, margin: 0.0)
|
|
87
|
+
|
|
88
|
+
total_ops = 100 * 100
|
|
89
|
+
expected_yards = total_ops * estimator.yards_per_stitch
|
|
90
|
+
|
|
91
|
+
assert_in_delta expected_yards, result[:yardage].to(:yards).value, 0.01
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_for_piece_skeins_ceil_to_whole_number
|
|
95
|
+
result = estimator.for_piece(stitches: 50.stitches, rows: 50.rows, margin: 0.0)
|
|
96
|
+
|
|
97
|
+
raw_yards = 50 * 50 * estimator.yards_per_stitch
|
|
98
|
+
expected_skeins = (raw_yards / 210.0).ceil
|
|
99
|
+
|
|
100
|
+
assert_equal expected_skeins, result[:skeins]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# -----------------------------
|
|
104
|
+
# for_rectangle
|
|
105
|
+
# -----------------------------
|
|
106
|
+
|
|
107
|
+
def test_for_rectangle_returns_yardage_and_skeins
|
|
108
|
+
result = estimator.for_rectangle(width: 20.inches, height: 30.inches)
|
|
109
|
+
|
|
110
|
+
assert result.key?(:yardage)
|
|
111
|
+
assert result.key?(:skeins)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_for_rectangle_uses_gauge_to_compute_stitches
|
|
115
|
+
rect_result = estimator.for_rectangle(width: 20.inches, height: 30.inches, margin: 0.0)
|
|
116
|
+
|
|
117
|
+
stitches = gauge.required_stitches(20.inches)
|
|
118
|
+
rows = gauge.required_rows(30.inches)
|
|
119
|
+
piece_result = estimator.for_piece(stitches: stitches, rows: rows, margin: 0.0)
|
|
120
|
+
|
|
121
|
+
assert_in_delta(
|
|
122
|
+
piece_result[:yardage].to(:yards).value,
|
|
123
|
+
rect_result[:yardage].to(:yards).value,
|
|
124
|
+
0.01
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_for_rectangle_with_custom_margin
|
|
129
|
+
no_margin = estimator.for_rectangle(width: 20.inches, height: 30.inches, margin: 0.0)
|
|
130
|
+
with_margin = estimator.for_rectangle(width: 20.inches, height: 30.inches, margin: 0.15)
|
|
131
|
+
|
|
132
|
+
assert_in_delta(
|
|
133
|
+
no_margin[:yardage].to(:yards).value * 1.15,
|
|
134
|
+
with_margin[:yardage].to(:yards).value,
|
|
135
|
+
0.01
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# -----------------------------
|
|
140
|
+
# realistic scenarios
|
|
141
|
+
# -----------------------------
|
|
142
|
+
|
|
143
|
+
def test_scarf_estimate_is_reasonable
|
|
144
|
+
# A scarf ~8" wide, 60" long in worsted weight
|
|
145
|
+
result = estimator.for_rectangle(width: 8.inches, height: 60.inches)
|
|
146
|
+
|
|
147
|
+
yards = result[:yardage].to(:yards).value
|
|
148
|
+
# A worsted scarf typically uses 200-400 yards
|
|
149
|
+
assert yards > 100, "yardage #{yards} seems too low for a scarf"
|
|
150
|
+
assert yards < 600, "yardage #{yards} seems too high for a scarf"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_blanket_estimate_is_reasonable
|
|
154
|
+
# A throw blanket ~50" x 60" in worsted weight
|
|
155
|
+
result = estimator.for_rectangle(width: 50.inches, height: 60.inches)
|
|
156
|
+
|
|
157
|
+
yards = result[:yardage].to(:yards).value
|
|
158
|
+
# A worsted throw typically uses 1500-3500 yards
|
|
159
|
+
assert yards > 1000, "yardage #{yards} seems too low for a blanket"
|
|
160
|
+
assert yards < 5000, "yardage #{yards} seems too high for a blanket"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_larger_piece_needs_more_yarn
|
|
164
|
+
small = estimator.for_rectangle(width: 10.inches, height: 10.inches)
|
|
165
|
+
large = estimator.for_rectangle(width: 20.inches, height: 20.inches)
|
|
166
|
+
|
|
167
|
+
assert large[:yardage].to(:yards).value > small[:yardage].to(:yards).value
|
|
168
|
+
assert large[:skeins] >= small[:skeins]
|
|
169
|
+
end
|
|
170
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yarn_skein
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Meagan Waller
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fiber_gauge
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: minitest
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,25 +65,31 @@ files:
|
|
|
51
65
|
- README.md
|
|
52
66
|
- Rakefile
|
|
53
67
|
- lib/yarn_skein.rb
|
|
68
|
+
- lib/yarn_skein/catalog.rb
|
|
69
|
+
- lib/yarn_skein/colorwork_estimator.rb
|
|
54
70
|
- lib/yarn_skein/fiber_blend.rb
|
|
55
71
|
- lib/yarn_skein/substitution.rb
|
|
56
72
|
- lib/yarn_skein/version.rb
|
|
57
73
|
- lib/yarn_skein/weight_category.rb
|
|
74
|
+
- lib/yarn_skein/yardage_estimator.rb
|
|
58
75
|
- lib/yarn_skein/yarn.rb
|
|
59
76
|
- sig/yarn_skein.rbs
|
|
77
|
+
- test/catalog_test.rb
|
|
78
|
+
- test/colorwork_estimator_test.rb
|
|
60
79
|
- test/fiber_blend_test.rb
|
|
61
80
|
- test/substitution_test.rb
|
|
62
81
|
- test/test_helper.rb
|
|
63
82
|
- test/version_test.rb
|
|
64
83
|
- test/weight_category_test.rb
|
|
84
|
+
- test/yardage_estimator_test.rb
|
|
65
85
|
- test/yarn_test.rb
|
|
66
|
-
homepage: https://github.com/meaganewaller/yarn_skein
|
|
86
|
+
homepage: https://github.com/meaganewaller/craftos/tree/main/gems/yarn_skein
|
|
67
87
|
licenses:
|
|
68
88
|
- MIT
|
|
69
89
|
metadata:
|
|
70
|
-
homepage_uri: https://github.com/meaganewaller/yarn_skein
|
|
71
|
-
source_code_uri: https://github.com/meaganewaller/yarn_skein
|
|
72
|
-
changelog_uri: https://github.com/meaganewaller/
|
|
90
|
+
homepage_uri: https://github.com/meaganewaller/craftos/tree/main/gems/yarn_skein
|
|
91
|
+
source_code_uri: https://github.com/meaganewaller/craftos/tree/main/gems/yarn_skein
|
|
92
|
+
changelog_uri: https://github.com/meaganewaller/craftos/tree/main/gems/yarn_skein/CHANGELOG.md
|
|
73
93
|
rdoc_options: []
|
|
74
94
|
require_paths:
|
|
75
95
|
- lib
|