Ruby
Crafting Code: Building a Ruby Pattern Generator for a Crochet Circle
In my time as a developer, I have noticed that one of the most common ways my coworkers spend time coding outside of work is by developing little code snippets or apps that solve problems in their everyday lives. From household budgeting, to managing workouts on rowing machines, to generating a Taco Bell order, these projects allow devs to explore different coding styles and learn new technologies.
For a long time, most of my side projects have been for the sole purpose of learning a new technology. When I wanted to start building mobile apps with React Native, I wrote a small to-do app that, once finished, I abandoned. The same thing happened when I wanted to try to use PostgreSQL's listen and notify feature to build a live updating chat app. So, when I was thinking about a new side project, I decided it was time to work on something that could be long lived and help me with one of my favorite hobbies: crocheting.
The Premise
Recently, I've been making a lot of small projects that have started with a base shape that then gets built upon. Often, this shape is a circle. After running through several projects, I started to notice a pattern of increases and repetitions for each row. It occurred to me that if the shape followed a specific pattern, I could probably build a ruby class to generate that pattern. Thus began this side project!
Breaking Down a Simplified Pattern
To begin, we have to inspect the pattern. Crochet patterns follow a specific format, and use abbreviations for the types of stitches being used.
The example pattern uses the following abbreviations and rules:
| Abbreviation | Meaning | Use | Stitch Count |
|---|---|---|---|
| sc | single crochet | adds a stitch to the round | 1 |
| inc | increase (two single crochets in the same stitch) | adds an extra stitch to the round | 2 |
With those abbreviations in mind, we can start parsing the pattern.
R1: 6sc in magic ring (6)
R2: [inc] x6 (12)
R3: [sc, inc] x6 (18)
R4: sc, inc, [2sc, inc] x5, sc (24)
R5: [3sc, inc] x6 (30)
R6: 2sc, inc, [4sc, inc] x5, 2sc (36)
R7: [5sc, inc] x6 (42)
R8: 3sc, inc, [6sc, inc] x5, 3sc (48)
Without knowing the details of how crochet patterns work, we can still get an idea of the format here. First, we can see that each line starts with R{n}:. This is the indicator of the round we are working on. We can see here that the pattern ends on R8, so we know that we're going to have exactly 8 rounds to the circle.
Second, we can see that each line ends with a number in parentheses. This number represents the final stitch count for the round. Here is where we may begin to see the beginnings of a pattern. Looking at the stitch counts, we see that we're starting with 6 stitches, and then increasing by 6 in each round. Given that behavior, the equation for determining the stitch count based on a given row would be 6 + 6(R - 1), where R is the row number.
In between the round number and stitch count are the actual directions for the round. In some cases, such as round 3, there are square brackets around a stitch or set of stitches, followed by x{n}. This is an indicator that the stitch or stitches within the brackets are going to be repeated n times. So in round 4, where the directions are sc, inc, [2sc, inc] x5, sc, what we're actually doing is:
sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc
Starting the Ruby Class
Now that the stitch count equation has been determined, a small ruby class can start to be built up. The class will expect an input of how many rows, and output an array of strings in the correct format for each row. Also, given that we have an example pattern, an rspec test expecting the correct output can be written.
class CirclePatternGenerator
attr_reader :row_number
def self.generate(row_count:)
(1..row_count).map do |row_number|
new(row_number:).pattern
end
end
def initialize(row_number:)
@row_number = row_number
end
def pattern
"#{row_title}: #{instructions} (#{stitch_count})"
end
private
def row_title
"R#{row_number}"
end
def stitch_count
6 + 6 * (row_number - 1)
end
def instructions
end
end
RSpec.describe CirclePatternGenerator do
describe ".generate" do
let(:expected_output) do
[
"R1: 6sc in magic ring (6)",
"R2: [inc] x6 (12)",
"R3: [sc, inc] x6 (18)",
"R4: sc, inc, [2sc, inc] x5, sc (24)",
"R5: [3sc, inc] x6 (30)",
"R6: 2sc, inc, [4sc, inc] x5, 2sc (36)",
"R7: [5sc, inc] x6 (42)",
"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)",
]
end
it "outputs the correct pattern" do
expect(described_class.generate(row_count: 8)).to eq(expected_output)
end
end
end
Running the specs, we can confirm that the row titles and stitch counts are correct, and the instructions are the last thing to figure out.
$ rspec circle_pattern_generator.rb
Randomized with seed 47053
F
Failures:
1) CirclePatternGenerator.generate outputs the correct pattern
Failure/Error:
expect(described_class.generate(row_count: 8)).to eq(expected_output)
expected: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [sc, inc] x6 (18)", "R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)", "R7: [5sc, inc] x6 (42)", "R8: 3sc, inc, [6sc, inc] x5, 3sc (48)"]
got: ["R1: (6)", "R2: (12)", "R3: (18)", "R4: (24)", "R5: (30)", "R6: (36)", "R7: (42)", "R8: (48)"]
Examining the Instructions
The core instructions for each row will be the most involved part of this pattern generator. Knowing that each row is made up of single crochets and increases, we can break down the instructions for the rows into the totals for each kind of stitch:
| Row Number | Instructions | Single Crochets | Increases |
|---|---|---|---|
| 1 | 6sc in magic ring | 6 | 0 |
| 2 | [inc] x6 | 0 | 6 |
| 3 | [sc, inc] x6 | 6 | 6 |
| 4 | sc, inc, [2sc, inc] x5, sc | 12 | 6 |
| 5 | [3sc, inc] x6 | 18 | 6 |
| 6 | 2sc, inc, [4sc, inc] x5, 2sc | 24 | 6 |
| 7 | [5sc, inc] x6 | 30 | 6 |
| 8 | 3sc, inc, [6sc, inc] x5, 3sc | 36 | 6 |
Looking at the table, we can see that the number of increases is 6 starting from the second row, and doesn't change as the rows progress. It's also apparent that the number of single crochets increases by 6 per row after starting from 0 on the second row. The first row, therefore, appears to be an outlier to the pattern evident from lines 2 - 8. The first row is also the only row that has special instructions ("in magic ring").
If we treat the first row like a special case, we can add a guard clause to the instructions method on the ruby class.
class CirclePatternGenerator
# ...
def instructions
return first_row_instructions if first_row?
end
def first_row_instructions
"6sc in magic ring"
end
def first_row?
row_number == 1
end
end
Running the specs again, we can see that the first row now matches the expected output.
$ rspec circle_pattern_generator.rb
Randomized with seed 47053
F
Failures:
1) CirclePatternGenerator.generate outputs the correct pattern
Failure/Error:
expect(described_class.generate(row_count: 8)).to eq(expected_output)
expected: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [sc, inc] x6 (18)", "R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)", "R7: [5sc, inc] x6 (42)", "R8: 3sc, inc, [6sc, inc] x5, 3sc (48)"]
got: ["R1: 6sc in magic ring (6)", "R2: (12)", "R3: (18)", "R4: (24)", "R5: (30)", "R6: (36)", "R7: (42)", "R8: (48)"]
With the special case out of the way, it's time to look into the rows that adhere to the pattern of single crochets and increases.
The most noticeable thing in the row instructions is the number of single crochets that are used to space out the increases. In row 2, there are none, but from there, we can see that row 3 has 1 single crochet between increases, followed by 2 in row 4, 3 in row 5, and so on. We have ourselves another small equation -- the number of single crochets spacing out the increases is R - 2, where R is our row number. We can add that as a method to use for the instructions in the ruby class.
def sc_count
row_number - 2
end
Returing to the pattern, it appears that the only difference in the way the instructions work is that in the odd numbered rows, we repeat the same thing 6 times, and in the even numbered rows, the repeat is only 5 times. The reason the repeat on the even rows is 5 times is because the single crochet spacing appears to be split between the beginning and end of the row. We again have a bit of an outlier with the second row, since it doesn't have any single crochets spacing out the increases.
# no sc
R2: [inc] x6 (12)
# sc in the beginning and end, totaling 2sc
R4: sc, inc, [2sc, inc] x5, sc (24)
# 2sc in the beginning and end, totaling 4sc
R6: 2sc, inc, [4sc, inc] x5, 2sc (36)
# 3sc in the beginning and end, totaling 6sc
R8: 3sc, inc, [6sc, inc] x5, 3sc (48)
Since we have another outlier, it seems easiest to add a second guard clause to the class.
class CirclePatternGenerator
# ...
def instructions
return first_row_instructions if first_row?
return second_row_instructions if second_row?
end
# ...
def second_row_instructions
"[inc] x6"
end
def second_row?
row_number == 2
end
end
Now, we can build out the logic for the even and odd rows, starting with the easier option: the odd rows.
Programming the Odd Rows
Using the sc_count method determined earlier, the odd numbered rows should follow the pattern of "[#{sc_count}sc, inc] x6".
class CirclePatternGenerator
# ...
def instructions
return first_row_instructions if first_row?
return second_row_instructions if second_row?
if row_number.odd?
odd_row_instructions
end
end
def odd_row_instructions
"[#{sc_count}sc, inc] x6"
end
# ...
end
Running the tests again, we see that this almost looks right, except that there shouldn't be a 1 in front of the sc for row 3 ("R3: [1sc, inc] x6 (18)").
$ rspec circle_pattern_generator.rb
Randomized with seed 37350
F
Failures:
1) CirclePatternGenerator.generate outputs the correct pattern
Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)
expected: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [sc, inc] x6 (18)", "R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)", "R7: [5sc, inc] x6 (42)", "R8: 3sc, inc, [6sc, inc] x5, 3sc (48)"]
got: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [1sc, inc] x6 (18)", "R4: (24)", "R5: [3sc, inc] x6 (30)", "R6: (36)", "R7: [5sc, inc] x6 (42)", "R8: (48)"]
While we can fix this for row 3, this isn't the only case where there's just one single crochet. Perhaps it would be worth creating a method to handle the display of the single crochets.
class CirclePatternGenerator
# ...
def odd_row_instructions
"[#{sc_pattern(sc_count)}, inc] x6"
end
def sc_pattern(number)
(number == 1) ? "sc" : "#{number}sc"
end
# ...
end
Rerunning the tests, we can see that this solves the issue for row 3.
$ rspec circle_pattern_generator.rb
Randomized with seed 45147
F
Failures:
1) CirclePatternGenerator.generate outputs the correct pattern
Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)
expected: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [sc, inc] x6 (18)", "R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)", "R7: [5sc, inc] x6 (42)", "R8: 3sc, inc, [6sc, inc] x5, 3sc (48)"]
got: ["R1: 6sc in magic ring (6)", "R2: [inc] x6 (12)", "R3: [sc, inc] x6 (18)", "R4: (24)", "R5: [3sc, inc] x6 (30)", "R6: (36)", "R7: [5sc, inc] x6 (42)", "R8: (48)"]
Now we can address the even rows.
Programming the Even Rows
As noted before, the even rows repeat 5 times, and have the 6th repetition split across the beginning and end of the row.
class CirclePatternGenerator
# ...
def instructions
return first_row_instructions if first_row?
return second_row_instructions if second_row?
row_number.odd? ? odd_row_instructions : even_row_instructions
end
def even_row_instructions
split_pattern = sc_pattern(sc_count / 2)
"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}"
end
# ...
end
Running the test again, we have green! We have created a pattern generator for crochet circles based on a specified row count.
class CirclePatternGenerator
attr_reader :row_number
def self.generate(row_count:)
(1..row_count).map do |row_number|
new(row_number:).pattern
end
end
def initialize(row_number:)
@row_number = row_number
end
def pattern
"#{row_title}: #{instructions} (#{stitch_count})"
end
private
def row_title
"R#{row_number}"
end
def stitch_count
6 + 6 * (row_number - 1)
end
def instructions
return first_row_instructions if first_row?
return second_row_instructions if second_row?
row_number.odd? ? odd_row_instructions : even_row_instructions
end
def odd_row_instructions
"[#{sc_pattern(sc_count)}, inc] x6"
end
def even_row_instructions
split_pattern = sc_pattern(sc_count / 2)
"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}"
end
def sc_pattern(number)
(number == 1) ? "sc" : "#{number}sc"
end
def sc_count
row_number - 2
end
def first_row_instructions
"6sc in magic ring"
end
def first_row?
row_number == 1
end
def second_row_instructions
"[inc] x6"
end
def second_row?
row_number == 2
end
end
Takeaways
This was a fun little experiment, but not entirely useful on its own. However, what if 3D shapes in crochet also follow a similar pattern? Would I be able to generate spheres? Or cubes? Would a row count really be the ideal entry point for such shapes? It seems that more research and experimentation are needed!