yarn_skein 0.1.1 → 0.2.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 +8 -0
- data/Rakefile +5 -53
- data/lib/yarn_skein/substitution.rb +30 -0
- data/lib/yarn_skein/version.rb +3 -1
- data/lib/yarn_skein.rb +1 -0
- data/test/fiber_blend_test.rb +92 -0
- data/test/substitution_test.rb +124 -0
- data/test/test_helper.rb +9 -0
- data/test/version_test.rb +10 -0
- data/test/weight_category_test.rb +37 -0
- data/test/yarn_test.rb +229 -0
- metadata +24 -7
- data/.github/actions/ruby-setup/action.yml +0 -23
- data/.github/dependabot.yml +0 -8
- data/.github/workflows/ci.yml +0 -24
- data/.github/workflows/release.yml +0 -61
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de030bc1badf27891aebbba5deb39a73ab035fbe4be31842f4e9d2bb8105ecdb
|
|
4
|
+
data.tar.gz: 9ae3781907723962ea50c3b25a3f98705c321f251f981a6a558b3a81218ed442
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c4bbcb7d360334e2f79d733f4d53d2371414f2c6e73bc48b67d1a3e027e2c48a1e822d56fbc424ef6c42dfacbbcd471f00ab55497baac641ea510f4564748a0
|
|
7
|
+
data.tar.gz: cc0e8ac28ad96f6977471b3fca483989ca8b0e2142040dce66632714c0c3b022d972ec363f395af645f4143a41757357bf48ef362833a399a183d08a0093b708
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.2.0] - 2026-03-21
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- `YarnSkein::Substitution` for finding compatible yarn substitutes from a catalog
|
|
8
|
+
- Matches by weight category and grist tolerance (default 15%, configurable)
|
|
9
|
+
- Optional fiber content filter
|
|
10
|
+
- Added missing `rake` and `simplecov-json` dev dependencies
|
|
11
|
+
|
|
4
12
|
## [0.1.1] - 2026-03-14
|
|
5
13
|
### Added
|
|
6
14
|
- `YarnSkein::Yarn` for modeling a skein's brand, line, yardage, skein weight, and optional fiber content
|
data/Rakefile
CHANGED
|
@@ -1,58 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "rspec/core/rake_task"
|
|
5
|
-
require "yard"
|
|
6
|
-
require "yard/rake/yardoc_task"
|
|
7
|
-
require "standard/rake"
|
|
3
|
+
require "rake/testtask"
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
desc "Generate YARD documentation"
|
|
13
|
-
YARD::Rake::YardocTask.new(:generate) do |t|
|
|
14
|
-
t.files = ["lib/**/*.rb", "-", "README.md"]
|
|
15
|
-
t.options = [
|
|
16
|
-
"--output-dir", "doc/yard",
|
|
17
|
-
"--markup", "markdown",
|
|
18
|
-
"--title", "YarnSkein - Yarn metadata and skein calculations",
|
|
19
|
-
"--readme", "README.md"
|
|
20
|
-
]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
desc "Regenerate documentation with cache reset"
|
|
24
|
-
task regenerate: ["doc:clean", "doc:generate"]
|
|
25
|
-
|
|
26
|
-
desc "Clean generated documentation"
|
|
27
|
-
task :clean do
|
|
28
|
-
rm_rf "doc/yard"
|
|
29
|
-
rm_rf ".yardoc"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
desc "Start YARD server for local documentation viewing"
|
|
33
|
-
task :serve do
|
|
34
|
-
sh "bundle exec yard server --reload --port 8808"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
desc "Validate YARD documentation coverage"
|
|
38
|
-
task :coverage do
|
|
39
|
-
sh "bundle exec yard stats --list-undoc"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
desc "Generate complete documentation with coverage report"
|
|
43
|
-
task complete: [:generate, :coverage]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Add shorthand aliases
|
|
47
|
-
task yard: "doc:generate"
|
|
48
|
-
task yard_server: "doc:serve"
|
|
49
|
-
task yard_clean: "doc:clean"
|
|
50
|
-
rescue LoadError
|
|
51
|
-
# YARD is only available in development/test environments
|
|
52
|
-
# Silence this warning in production where it's not needed
|
|
5
|
+
Rake::TestTask.new do |t|
|
|
6
|
+
t.libs << "test"
|
|
7
|
+
t.pattern = "test/**/*_test.rb"
|
|
53
8
|
end
|
|
54
9
|
|
|
55
|
-
task :
|
|
56
|
-
Rake::Task["standard"].invoke
|
|
57
|
-
Rake::Task["spec"].invoke
|
|
58
|
-
end
|
|
10
|
+
task default: :test
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module YarnSkein
|
|
2
|
+
class Substitution
|
|
3
|
+
DEFAULT_TOLERANCE = 0.15
|
|
4
|
+
|
|
5
|
+
attr_reader :target, :catalog
|
|
6
|
+
|
|
7
|
+
def initialize(target:, catalog:)
|
|
8
|
+
@target = target
|
|
9
|
+
@catalog = catalog
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def matches(tolerance: DEFAULT_TOLERANCE, fiber: nil)
|
|
13
|
+
catalog.select do |yarn|
|
|
14
|
+
next false if yarn == target
|
|
15
|
+
next false unless yarn.weight_category == target.weight_category
|
|
16
|
+
next false unless within_grist_tolerance?(yarn, tolerance)
|
|
17
|
+
next false if fiber && !yarn.fiber_content&.contains?(fiber)
|
|
18
|
+
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def within_grist_tolerance?(yarn, tolerance)
|
|
26
|
+
ratio = yarn.yards_per_100g / target.yards_per_100g.to_f
|
|
27
|
+
ratio.between?(1.0 - tolerance, 1.0 + tolerance)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/yarn_skein/version.rb
CHANGED
data/lib/yarn_skein.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "yarn_skein/version"
|
|
|
5
5
|
require_relative "yarn_skein/weight_category"
|
|
6
6
|
require_relative "yarn_skein/fiber_blend"
|
|
7
7
|
require_relative "yarn_skein/yarn"
|
|
8
|
+
require_relative "yarn_skein/substitution"
|
|
8
9
|
|
|
9
10
|
# Domain objects for describing yarn skeins, fiber blends, and weight classes.
|
|
10
11
|
#
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinFiberBlendTest < Minitest::Test
|
|
6
|
+
# -----------------------------
|
|
7
|
+
# Initialization
|
|
8
|
+
# -----------------------------
|
|
9
|
+
|
|
10
|
+
def test_stores_fiber_percentages
|
|
11
|
+
blend = YarnSkein::FiberBlend.new(
|
|
12
|
+
merino_wool: 80,
|
|
13
|
+
nylon: 20
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
assert_equal(
|
|
17
|
+
{
|
|
18
|
+
merino_wool: 80,
|
|
19
|
+
nylon: 20
|
|
20
|
+
},
|
|
21
|
+
blend.fibers
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_raises_error_if_percentages_do_not_sum_to_100
|
|
26
|
+
error = assert_raises(ArgumentError) do
|
|
27
|
+
YarnSkein::FiberBlend.new(
|
|
28
|
+
merino_wool: 50,
|
|
29
|
+
nylon: 30
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
assert_match(/must sum to 100/, error.message)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# -----------------------------
|
|
37
|
+
# yards_per_100g
|
|
38
|
+
# -----------------------------
|
|
39
|
+
|
|
40
|
+
def test_calculates_yards_per_100_grams
|
|
41
|
+
yarn = YarnSkein::Yarn.new(
|
|
42
|
+
brand: "Malabrigo",
|
|
43
|
+
line: "Rios",
|
|
44
|
+
yardage: 210.yards,
|
|
45
|
+
skein_weight: 100.grams
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert_equal 210, yarn.yards_per_100g
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# -----------------------------
|
|
52
|
+
# percentage
|
|
53
|
+
# -----------------------------
|
|
54
|
+
|
|
55
|
+
def test_returns_percentage_of_fiber
|
|
56
|
+
blend = YarnSkein::FiberBlend.new(
|
|
57
|
+
merino_wool: 80,
|
|
58
|
+
nylon: 20
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assert_equal 80, blend.percentage(:merino_wool)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_returns_zero_for_missing_fibers
|
|
65
|
+
blend = YarnSkein::FiberBlend.new(
|
|
66
|
+
merino_wool: 100
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert_equal 0, blend.percentage(:nylon)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# -----------------------------
|
|
73
|
+
# contains?
|
|
74
|
+
# -----------------------------
|
|
75
|
+
|
|
76
|
+
def test_contains_returns_true_if_fiber_exists
|
|
77
|
+
blend = YarnSkein::FiberBlend.new(
|
|
78
|
+
merino_wool: 80,
|
|
79
|
+
nylon: 20
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert blend.contains?(:nylon)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_contains_returns_false_if_fiber_does_not_exist
|
|
86
|
+
blend = YarnSkein::FiberBlend.new(
|
|
87
|
+
merino_wool: 100
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
refute blend.contains?(:nylon)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinSubstitutionTest < Minitest::Test
|
|
6
|
+
def build_yarn(line:, yardage:, skein_weight:, fiber_content: nil)
|
|
7
|
+
YarnSkein::Yarn.new(
|
|
8
|
+
brand: "TestBrand",
|
|
9
|
+
line: line,
|
|
10
|
+
yardage: yardage,
|
|
11
|
+
skein_weight: skein_weight,
|
|
12
|
+
fiber_content: fiber_content
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def target_yarn
|
|
17
|
+
@target_yarn ||= build_yarn(line: "Target", yardage: 210.yards, skein_weight: 100.grams)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 210 yards/100g => worsted, yards_per_100g = 210
|
|
21
|
+
|
|
22
|
+
def close_match
|
|
23
|
+
@close_match ||= build_yarn(line: "Close", yardage: 200.yards, skein_weight: 100.grams)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def exact_match
|
|
27
|
+
@exact_match ||= build_yarn(line: "Exact", yardage: 210.yards, skein_weight: 100.grams)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def too_far
|
|
31
|
+
@too_far ||= build_yarn(line: "TooFar", yardage: 300.yards, skein_weight: 100.grams)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def different_category
|
|
35
|
+
@different_category ||= build_yarn(line: "Bulky", yardage: 120.yards, skein_weight: 100.grams)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def wool_yarn
|
|
39
|
+
@wool_yarn ||= build_yarn(
|
|
40
|
+
line: "Wool",
|
|
41
|
+
yardage: 200.yards,
|
|
42
|
+
skein_weight: 100.grams,
|
|
43
|
+
fiber_content: YarnSkein::FiberBlend.new(wool: 100)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cotton_yarn
|
|
48
|
+
@cotton_yarn ||= build_yarn(
|
|
49
|
+
line: "Cotton",
|
|
50
|
+
yardage: 200.yards,
|
|
51
|
+
skein_weight: 100.grams,
|
|
52
|
+
fiber_content: YarnSkein::FiberBlend.new(cotton: 100)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# -----------------------------
|
|
57
|
+
# matches
|
|
58
|
+
# -----------------------------
|
|
59
|
+
|
|
60
|
+
def test_returns_yarns_matching_weight_category_and_grist
|
|
61
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [close_match, too_far])
|
|
62
|
+
|
|
63
|
+
assert_equal [close_match], sub.matches
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_excludes_target_yarn_from_results
|
|
67
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [target_yarn, close_match])
|
|
68
|
+
|
|
69
|
+
assert_equal [close_match], sub.matches
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_excludes_different_weight_category
|
|
73
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [different_category])
|
|
74
|
+
|
|
75
|
+
assert_empty sub.matches
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_returns_empty_for_empty_catalog
|
|
79
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [])
|
|
80
|
+
|
|
81
|
+
assert_empty sub.matches
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_returns_empty_when_no_matches
|
|
85
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [too_far, different_category])
|
|
86
|
+
|
|
87
|
+
assert_empty sub.matches
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# -----------------------------
|
|
91
|
+
# tolerance
|
|
92
|
+
# -----------------------------
|
|
93
|
+
|
|
94
|
+
def test_configurable_tolerance
|
|
95
|
+
# close_match is 200/210 = ~0.952, within 15% but outside 2%
|
|
96
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [close_match])
|
|
97
|
+
|
|
98
|
+
assert_equal [close_match], sub.matches(tolerance: 0.15)
|
|
99
|
+
assert_empty sub.matches(tolerance: 0.02)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# -----------------------------
|
|
103
|
+
# fiber filter
|
|
104
|
+
# -----------------------------
|
|
105
|
+
|
|
106
|
+
def test_filters_by_fiber_content
|
|
107
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [wool_yarn, cotton_yarn])
|
|
108
|
+
|
|
109
|
+
assert_equal [wool_yarn], sub.matches(fiber: :wool)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_fiber_filter_excludes_yarns_without_fiber_content
|
|
113
|
+
yarn_no_fiber = build_yarn(line: "NoFiber", yardage: 200.yards, skein_weight: 100.grams)
|
|
114
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [yarn_no_fiber, wool_yarn])
|
|
115
|
+
|
|
116
|
+
assert_equal [wool_yarn], sub.matches(fiber: :wool)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_includes_exact_grist_match
|
|
120
|
+
sub = YarnSkein::Substitution.new(target: target_yarn, catalog: [exact_match])
|
|
121
|
+
|
|
122
|
+
assert_equal [exact_match], sub.matches
|
|
123
|
+
end
|
|
124
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinWeightCategoryTest < Minitest::Test
|
|
6
|
+
def test_returns_lace_for_1000_yards_per_100g
|
|
7
|
+
assert_equal :lace, YarnSkein::WeightCategory.for(1000)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_returns_fingering_for_440_yards_per_100g
|
|
11
|
+
assert_equal :fingering, YarnSkein::WeightCategory.for(440)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_returns_sport_for_320_yards_per_100g
|
|
15
|
+
assert_equal :sport, YarnSkein::WeightCategory.for(320)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_returns_dk_for_250_yards_per_100g
|
|
19
|
+
assert_equal :dk, YarnSkein::WeightCategory.for(250)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_returns_worsted_for_210_yards_per_100g
|
|
23
|
+
assert_equal :worsted, YarnSkein::WeightCategory.for(210)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_returns_bulky_for_120_yards_per_100g
|
|
27
|
+
assert_equal :bulky, YarnSkein::WeightCategory.for(120)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_returns_super_bulky_for_90_yards_per_100g
|
|
31
|
+
assert_equal :super_bulky, YarnSkein::WeightCategory.for(90)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_returns_nil_for_negative_yards_per_100g
|
|
35
|
+
assert_nil YarnSkein::WeightCategory.for(-10)
|
|
36
|
+
end
|
|
37
|
+
end
|
data/test/yarn_test.rb
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class YarnSkeinYarnTest < Minitest::Test
|
|
6
|
+
def build_yarn(**attrs)
|
|
7
|
+
YarnSkein::Yarn.new(
|
|
8
|
+
brand: "Malabrigo",
|
|
9
|
+
line: "Rios",
|
|
10
|
+
yardage: 210.yards,
|
|
11
|
+
skein_weight: 100.grams,
|
|
12
|
+
**attrs
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# -----------------------------
|
|
17
|
+
# Initialization
|
|
18
|
+
# -----------------------------
|
|
19
|
+
|
|
20
|
+
def test_initialization_stores_attributes
|
|
21
|
+
yarn = build_yarn
|
|
22
|
+
|
|
23
|
+
assert_equal "Malabrigo", yarn.brand
|
|
24
|
+
assert_equal "Rios", yarn.line
|
|
25
|
+
assert_equal 210.yards, yarn.yardage
|
|
26
|
+
assert_equal 100.grams, yarn.skein_weight
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# -----------------------------
|
|
30
|
+
# Grist
|
|
31
|
+
# -----------------------------
|
|
32
|
+
|
|
33
|
+
def test_grist_returns_ratio
|
|
34
|
+
yarn = build_yarn
|
|
35
|
+
|
|
36
|
+
assert_instance_of FiberUnits::Ratio, yarn.grist
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# -----------------------------
|
|
40
|
+
# Weight category
|
|
41
|
+
# -----------------------------
|
|
42
|
+
|
|
43
|
+
def test_weight_category_worsted
|
|
44
|
+
yarn = build_yarn
|
|
45
|
+
|
|
46
|
+
assert_equal :worsted, yarn.weight_category
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_weight_category_fingering
|
|
50
|
+
yarn = build_yarn(
|
|
51
|
+
line: "Sock",
|
|
52
|
+
yardage: 400.yards
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert_equal :fingering, yarn.weight_category
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_weight_category_bulky
|
|
59
|
+
yarn = build_yarn(
|
|
60
|
+
line: "Chunky",
|
|
61
|
+
yardage: 100.yards
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert_equal :bulky, yarn.weight_category
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# -----------------------------
|
|
68
|
+
# Validation
|
|
69
|
+
# -----------------------------
|
|
70
|
+
|
|
71
|
+
def test_requires_yardage_to_be_length
|
|
72
|
+
error = assert_raises(ArgumentError) do
|
|
73
|
+
YarnSkein::Yarn.new(
|
|
74
|
+
brand: "Malabrigo",
|
|
75
|
+
line: "Rios",
|
|
76
|
+
yardage: 210,
|
|
77
|
+
skein_weight: 100.grams
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
assert_match(/yardage/i, error.message)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_requires_skein_weight_to_be_weight
|
|
85
|
+
error = assert_raises(ArgumentError) do
|
|
86
|
+
YarnSkein::Yarn.new(
|
|
87
|
+
brand: "Malabrigo",
|
|
88
|
+
line: "Rios",
|
|
89
|
+
yardage: 210.yards,
|
|
90
|
+
skein_weight: 100
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
assert_match(/skein_weight/i, error.message)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# -----------------------------
|
|
98
|
+
# yards_per_100g
|
|
99
|
+
# -----------------------------
|
|
100
|
+
|
|
101
|
+
def test_calculates_yards_per_100g
|
|
102
|
+
yarn = build_yarn
|
|
103
|
+
|
|
104
|
+
assert_equal 210, yarn.yards_per_100g
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# -----------------------------
|
|
108
|
+
# similar_weight_to?
|
|
109
|
+
# -----------------------------
|
|
110
|
+
|
|
111
|
+
def test_similar_weight_for_same_category
|
|
112
|
+
rios = build_yarn
|
|
113
|
+
|
|
114
|
+
cascade = YarnSkein::Yarn.new(
|
|
115
|
+
brand: "Cascade",
|
|
116
|
+
line: "220",
|
|
117
|
+
yardage: 220.yards,
|
|
118
|
+
skein_weight: 100.grams
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert rios.similar_weight_to?(cascade)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_not_similar_for_different_categories
|
|
125
|
+
rios = build_yarn
|
|
126
|
+
|
|
127
|
+
sock = YarnSkein::Yarn.new(
|
|
128
|
+
brand: "Malabrigo",
|
|
129
|
+
line: "Sock",
|
|
130
|
+
yardage: 440.yards,
|
|
131
|
+
skein_weight: 100.grams
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
refute rios.similar_weight_to?(sock)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_not_similar_for_very_different_yarns
|
|
138
|
+
rios = build_yarn
|
|
139
|
+
|
|
140
|
+
lace = YarnSkein::Yarn.new(
|
|
141
|
+
brand: "Malabrigo",
|
|
142
|
+
line: "Lace",
|
|
143
|
+
yardage: 800.yards,
|
|
144
|
+
skein_weight: 100.grams
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
refute rios.similar_weight_to?(lace)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# -----------------------------
|
|
151
|
+
# skeins_required
|
|
152
|
+
# -----------------------------
|
|
153
|
+
|
|
154
|
+
def test_skeins_required_exact
|
|
155
|
+
yarn = build_yarn
|
|
156
|
+
|
|
157
|
+
assert_equal 2, yarn.skeins_required(420.yards)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_skeins_required_rounds_up
|
|
161
|
+
yarn = build_yarn
|
|
162
|
+
|
|
163
|
+
assert_equal 2, yarn.skeins_required(300.yards)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_skeins_required_handles_unit_conversion
|
|
167
|
+
yarn = build_yarn
|
|
168
|
+
|
|
169
|
+
assert_equal 2, yarn.skeins_required(384.meters)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_skeins_required_requires_length
|
|
173
|
+
yarn = build_yarn
|
|
174
|
+
|
|
175
|
+
assert_raises(ArgumentError) do
|
|
176
|
+
yarn.skeins_required(300)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# -----------------------------
|
|
181
|
+
# total_yardage
|
|
182
|
+
# -----------------------------
|
|
183
|
+
|
|
184
|
+
def test_total_yardage_returns_length
|
|
185
|
+
yarn = build_yarn
|
|
186
|
+
|
|
187
|
+
result = yarn.total_yardage(2)
|
|
188
|
+
|
|
189
|
+
assert_instance_of FiberUnits::Length, result
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def test_total_yardage_calculates_correct_value
|
|
193
|
+
yarn = build_yarn
|
|
194
|
+
|
|
195
|
+
assert_equal 630.yards, yarn.total_yardage(3)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_total_yardage_requires_positive_count
|
|
199
|
+
yarn = build_yarn
|
|
200
|
+
|
|
201
|
+
assert_raises(ArgumentError) do
|
|
202
|
+
yarn.total_yardage(0)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# -----------------------------
|
|
207
|
+
# Equality
|
|
208
|
+
# -----------------------------
|
|
209
|
+
|
|
210
|
+
def test_equal_yarns
|
|
211
|
+
yarn1 = build_yarn
|
|
212
|
+
yarn2 = build_yarn
|
|
213
|
+
|
|
214
|
+
assert_equal yarn1, yarn2
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_different_yarns_not_equal
|
|
218
|
+
yarn1 = build_yarn
|
|
219
|
+
|
|
220
|
+
yarn2 = YarnSkein::Yarn.new(
|
|
221
|
+
brand: "Malabrigo",
|
|
222
|
+
line: "Sock",
|
|
223
|
+
yardage: 400.yards,
|
|
224
|
+
skein_weight: 100.grams
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
refute_equal yarn1, yarn2
|
|
228
|
+
end
|
|
229
|
+
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Meagan Waller
|
|
@@ -15,9 +15,23 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
18
|
+
version: '0.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - ">="
|
|
@@ -31,10 +45,6 @@ executables: []
|
|
|
31
45
|
extensions: []
|
|
32
46
|
extra_rdoc_files: []
|
|
33
47
|
files:
|
|
34
|
-
- ".github/actions/ruby-setup/action.yml"
|
|
35
|
-
- ".github/dependabot.yml"
|
|
36
|
-
- ".github/workflows/ci.yml"
|
|
37
|
-
- ".github/workflows/release.yml"
|
|
38
48
|
- CHANGELOG.md
|
|
39
49
|
- CODE_OF_CONDUCT.md
|
|
40
50
|
- LICENSE
|
|
@@ -42,10 +52,17 @@ files:
|
|
|
42
52
|
- Rakefile
|
|
43
53
|
- lib/yarn_skein.rb
|
|
44
54
|
- lib/yarn_skein/fiber_blend.rb
|
|
55
|
+
- lib/yarn_skein/substitution.rb
|
|
45
56
|
- lib/yarn_skein/version.rb
|
|
46
57
|
- lib/yarn_skein/weight_category.rb
|
|
47
58
|
- lib/yarn_skein/yarn.rb
|
|
48
59
|
- sig/yarn_skein.rbs
|
|
60
|
+
- test/fiber_blend_test.rb
|
|
61
|
+
- test/substitution_test.rb
|
|
62
|
+
- test/test_helper.rb
|
|
63
|
+
- test/version_test.rb
|
|
64
|
+
- test/weight_category_test.rb
|
|
65
|
+
- test/yarn_test.rb
|
|
49
66
|
homepage: https://github.com/meaganewaller/yarn_skein
|
|
50
67
|
licenses:
|
|
51
68
|
- MIT
|
|
@@ -67,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
84
|
- !ruby/object:Gem::Version
|
|
68
85
|
version: '0'
|
|
69
86
|
requirements: []
|
|
70
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 3.6.9
|
|
71
88
|
specification_version: 4
|
|
72
89
|
summary: Typed measurements for fiber arts.
|
|
73
90
|
test_files: []
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
name: Ruby Setup
|
|
2
|
-
description: Setup Ruby and install dependencies
|
|
3
|
-
|
|
4
|
-
runs:
|
|
5
|
-
using: composite
|
|
6
|
-
steps:
|
|
7
|
-
- uses: actions/checkout@v5
|
|
8
|
-
|
|
9
|
-
- uses: jdx/mise-action@v3
|
|
10
|
-
with:
|
|
11
|
-
install: true
|
|
12
|
-
cache: true
|
|
13
|
-
mise_toml: |
|
|
14
|
-
[tools]
|
|
15
|
-
ruby = "4.0.1"
|
|
16
|
-
|
|
17
|
-
- uses: actions/cache@v4
|
|
18
|
-
with:
|
|
19
|
-
path: vendor/bundle
|
|
20
|
-
key: bundle-${{ runner.os }}-${{ hashFiles('Gemfile.lock') }}
|
|
21
|
-
|
|
22
|
-
- run: bundle install --jobs 4 --retry 3
|
|
23
|
-
shell: bash
|
data/.github/dependabot.yml
DELETED
data/.github/workflows/ci.yml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
env:
|
|
3
|
-
BUNDLE_PATH: vendor/bundle
|
|
4
|
-
|
|
5
|
-
on:
|
|
6
|
-
push:
|
|
7
|
-
branches:
|
|
8
|
-
- main
|
|
9
|
-
pull_request:
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
lint:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v5
|
|
16
|
-
- uses: ./.github/actions/ruby-setup
|
|
17
|
-
- run: bundle exec standardrb
|
|
18
|
-
|
|
19
|
-
test:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v5
|
|
23
|
-
- uses: ./.github/actions/ruby-setup
|
|
24
|
-
- run: bundle exec rspec
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v5
|
|
17
|
-
|
|
18
|
-
- name: Extract version from tag
|
|
19
|
-
id: version
|
|
20
|
-
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
21
|
-
|
|
22
|
-
- name: Verify gem version matches tag
|
|
23
|
-
run: |
|
|
24
|
-
GEM_VERSION=$(ruby -e "puts Gem::Specification.load('yarn_skein.gemspec').version")
|
|
25
|
-
if [ "$GEM_VERSION" != "${{ steps.version.outputs.VERSION }}" ]; then
|
|
26
|
-
echo "❌ Version mismatch: gemspec=$GEM_VERSION tag=${{ steps.version.outputs.VERSION }}"
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
|
29
|
-
echo "✅ Gem version matches tag"
|
|
30
|
-
|
|
31
|
-
- name: Verify changelog entry
|
|
32
|
-
run: |
|
|
33
|
-
if ! grep -q "## \[${{ steps.version.outputs.VERSION }}\]" CHANGELOG.md; then
|
|
34
|
-
echo "❌ Version ${{ steps.version.outputs.VERSION }} not found in CHANGELOG.md"
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
echo "✅ Changelog entry verified for ${{ steps.version.outputs.VERSION }}"
|
|
38
|
-
|
|
39
|
-
- name: Setup Ruby
|
|
40
|
-
uses: ruby/setup-ruby@v1
|
|
41
|
-
with:
|
|
42
|
-
ruby-version: "4.0.1"
|
|
43
|
-
bundler-cache: true
|
|
44
|
-
|
|
45
|
-
- name: Build gem
|
|
46
|
-
run: gem build yarn_skein.gemspec
|
|
47
|
-
|
|
48
|
-
- name: Configure RubyGems credentials
|
|
49
|
-
run: |
|
|
50
|
-
mkdir -p ~/.gem
|
|
51
|
-
echo ":rubygems_api_key: ${{ secrets.RUBYGEMS_API_KEY }}" > ~/.gem/credentials
|
|
52
|
-
chmod 0600 ~/.gem/credentials
|
|
53
|
-
|
|
54
|
-
- name: Publish gem
|
|
55
|
-
run: gem push *.gem
|
|
56
|
-
|
|
57
|
-
- name: Create GitHub release
|
|
58
|
-
uses: softprops/action-gh-release@v2
|
|
59
|
-
with:
|
|
60
|
-
tag_name: ${{ github.ref_name }}
|
|
61
|
-
generate_release_notes: true
|