Coding challenges: Is it about the solution, or the refactoring?

April 8, 2025 by Andrew Dawes

Intro

I love a good coding challenge. Some of my favorites come from the creatively phrased “Advent of Code” puzzles.

Recently, I’ve been wrestling through how I define my own sense of “success” as I complete challenges like these.

What does it look like for me to be doing a “fine” job solving these problems, vs doing a “good” job solving them?

Defining success

There are many distinct ways coding puzzle-solvers might choose to define success.

An incomplete list:

  • Consistency
  • Level of difficulty
  • Creativity
  • Correctness
  • Completeness
    • Including covering any “edge cases”
  • Performance
  • Time to solve
  • Brevity
    • AKA, “code golf”
  • Neatness
  • Readability
  • Adaptabilty
    • Or “evolutionary”
  • Reusability
  • Learning

Most puzzle-solvers treat coding challenges as a “race”. They measure success by how quickly they can complete each challenge as it arrives, either consciously or subconsciously comparing their “time to solve” against previous outcomes – such as times posted by others, their own times from previous attempts, or an overall running average of how long it takes them to solve similarly-formatted coding challenges .

Many other puzzle-solvers pride themselves on the difficulty of challenges. This trends towards what I would classify as “domain-specific” – puzzles that require you to have some knowledge outside of programming itself – usually math or linguistic.

Some puzzle-solvers focus on brevity – how few characters can be used to solve the problem? At the end of the day, these solutions produce the same result as more readable or more performant solutions. They usually require some cleverness or special insight into the choice or programming language.

An undervalued measure of success: Adaptability

One quality that I feel is often overlooked and undervalued is the adaptability or evolutionary quality of the solution.

In the real world of software development, this quality ranks high among the most important qualities of any solution. It’s called “software” for a reason – the nature of it is to be constantly evolving. There are probably very few circumstances where a piece of software will never need to change again, and very many situations where any impediment towards change will result in negative consequences to the developers and end users.

How to achieve adaptibility

So many books (Refactoring, Design Patterns – Gang of 4, Clean Code, The Pragmatic Programmer, Evolutionary Architecture), principles (SOLID), and best practices crowd this topic of “adaptibility”.

I think one fact everyone could agree on is that the activity of “refactoring” exercises some unique muscles in a programmer’s brain that are very helpful in empowering that programmer to design and build evolutionary software. And oftentimes (though certainly not always!), the outcomes of refactoring are positive changes towards making the refactored piece of software more adaptible to future demands.

I’ve been a big proponent of intentional, justified refactoring in projects I’ve worked on. However, it was only recently that I started thinking more deeply about the value of refactoring in coding challenges.

It’s not that I never refactored while completing coding challenges. To the contrary, I did it quite a lot! However, I always felt a bit guilty spending time refactoring vs “solving the problem itself”. I think what was really happening was I was overvaluing certain other measures of a solution’s success (level of difficulty, time to solve, brevity, performance) and undervaluing its adaptibility. And it’s easy to see why – after all, why would code I wrote to solve a one-off challenge need to be adaptible? When would I ever touch that code again?

A game of change

Enter Advent of Code.

One thing I really appreciate about Advent of Code is every challenge (at least, every challenge I have completed thus far) is actually multipart.

First, you are given a brand new challenge, with new concepts and requirements.

Something like, “Find the number of elves in Santa’s workshop who are wearing green hats. The floorplan of Santa’s 1-level workshop is represented as a 2-dimensional matrix”.

And so, you set about solving the challenge, creating a function which will seek through a 2 dimensional data structure, finding elves (not reindeer!), and counting only those with green hats.

You solve the solution, type the answer into the Advent of Code site, earn a gold star, and feel great about yourself.

But then comes the plot-twist:

“Santa recently outsourced the present-wrapping department to a firm of magical creatures based in Bangladesh. Their labor force occupies a multistory warehouse and wears a distinct uniform. Can you find the number of elves in the warehouse who are wearing a red turban? The floorplan of the Bangladesh sweatshop workshop is represented as a 3-dimensional matrix.”

It feels like looking through a kaleidoscope, where the subject matter of the challenge is fundamentally the same, but it is always assuming a new, fractal form.

I find this to be a lot of fun, because I am often surprised by what pieces of my initial solution I can vs cannot reuse. I always spend a little bit of time trying to anticipate. And I think that exercise is worth it – though, I would say my success rate has only improved a little over the years. However, I think at the end of the day, hindsight is 20/20. And in some ways, in that moment of hindsight is where the real learning happens – how can I rework this existing code to support both the old and the new requirements? And what design patterns (had I used them) would have best supported this new change – and will those patterns promise to support additional changes in the future?

Solve the problem, then refactor

Through this process, I’ve learned just how difficult it can be to predict the upcoming plot-twist. Instead of trying to see the future, I would say I now play more of a “balancing” game with myself. I try to solve the problem, within a reasonable amount of time, while still leaving myself room to evolve when the time comes.

And when the time does come, I’m not afraid to spend some time and thought refactoring.

Reuse code from previous solutions

Another practice that has really helped me to appreciate the value of evolutionary design and refactoring has been intentional reuse of code from previous challenges. As a start, I built a framework for quickly downloading each challenge’s input, running the solution, and dumping the output as JSON. As I have worked throught the challenges, I have developed a bit of a common library of solutions I reach for again and again – things like traversing a matrix or inverting columns to rows.

What it all means

I think my real-world experiences with refactoring have taught me that there is value in spending energy and time solving coding challenges in a way that emphasizes adaptability. I used to subconsciously feel refactoring puzzle solutions was a waste of my time, but now I see it as one of the most valuable (and downright fun) things I could spend my time doing.

An example

If you want to see an example, check out this diff where I refactored how I had solved the problem for 20163A to support additional requirements for 20163B.

diff --git a/src/Solution/Strategy/2016/3/a.ts b/src/Solution/Strategy/2016/3/a.ts
index b252d8a..9e4c7bc 100644
--- a/src/Solution/Strategy/2016/3/a.ts
+++ b/src/Solution/Strategy/2016/3/a.ts
@@ -1,5 +1,6 @@
 import InterfaceSolutionStrategy from '../../../Interface/Strategy.js';
 import InterfaceInputFetcher from '../../../../InputFetcher/Interface/Service.js';
+import { sideLengthsAreValid } from './Common.js';
 /*
 --- Day 3: Squares With Three Sides ---
 Now that you can think clearly, you move deeper into the labyrinth of hallways and office furniture that makes up this part of Easter Bunny HQ. This must be a graphic design department; the walls are covered in specifications for triangles.
@@ -36,21 +37,7 @@ class Solution20162a implements InterfaceSolutionStrategy {
             // Send the remaining to a (eventually memoized) function that sums all elems in array
             // Compare sum to number
             // Conditionally increment counter
-            let possible = true;
-            for (let index = 0; index < sideLengths.length; index++) {
-                const val = sideLengths[index];
-                if (
-                    val >=
-                    [
-                        ...sideLengths.slice(0, index),
-                        ...sideLengths.slice(index + 1),
-                    ].reduce((summed, toSum) => summed + toSum, 0)
-                ) {
-                    possible = false;
-                    break;
-                }
-            }
-            if (possible) {
+            if (sideLengthsAreValid(sideLengths)) {
                 ++possibleCount;
             }
         }
diff --git a/src/Library/Input/Transformer/ColumnToRow/C1.ts b/src/Library/Input/Transformer/ColumnToRow/C1.ts
new file mode 100644
index 0000000..1816386
--- /dev/null
+++ b/src/Library/Input/Transformer/ColumnToRow/C1.ts
@@ -0,0 +1,32 @@
+import { InputTransformerColumnToRow } from '../Interfaces.js';
+
+export default class C1 implements InputTransformerColumnToRow {
+    private windowSize: number;
+    private buffer: string[][];
+    private interstitialBuffer: string[][];
+    constructor(windowSize: number) {
+        this.windowSize = windowSize;
+        this.buffer = [];
+        this.interstitialBuffer = [];
+    }
+    public write(input: string[]) {
+        this.interstitialBuffer.push(input);
+        if (this.interstitialBuffer.length === this.windowSize) {
+            const transform: string[][] = [];
+            for (const row of this.interstitialBuffer.splice(0)) {
+                row.forEach((item, index) => {
+                    if (undefined === transform[index]) {
+                        transform[index] = [];
+                    }
+                    transform[index].push(item);
+                });
+            }
+            transform.forEach((val) => {
+                this.buffer.push(val);
+            });
+        }
+    }
+    public read(): string[][] {
+        return this.buffer.splice(0);
+    }
+}
diff --git a/src/Library/Input/Transformer/Interfaces.ts b/src/Library/Input/Transformer/Interfaces.ts
new file mode 100644
index 0000000..ce0ba92
--- /dev/null
+++ b/src/Library/Input/Transformer/Interfaces.ts
@@ -0,0 +1,4 @@
+export interface InputTransformerColumnToRow {
+    write(input: string[]): void;
+    read(): string[][];
+}
diff --git a/src/Solution/Factory/FromConfig.ts b/src/Solution/Factory/FromConfig.ts
index 12fd593..0fba4b1 100644
--- a/src/Solution/Factory/FromConfig.ts
+++ b/src/Solution/Factory/FromConfig.ts
@@ -11,6 +11,7 @@ import Solution20161b from '../Strategy/2016/1/b.js';
 import Solution20162a from '../Strategy/2016/2/a.js';
 import Solution20162b from '../Strategy/2016/2/b.js';
 import Solution20163a from '../Strategy/2016/3/a.js';
+import Solution20163b from '../Strategy/2016/3/b.js';
 class FromConfig implements InterfaceSolutionFactory {
     inputFetcherFactory: InterfaceInputFetcherFactory;
     constructor(inputFetcherFactory: InterfaceInputFetcherFactory) {
@@ -42,6 +43,8 @@ class FromConfig implements InterfaceSolutionFactory {
                 return new Solution20162b(service);
             case '20163a':
                 return new Solution20163a(service);
+            case '20163b':
+                return new Solution20163b(service);
             default:
                 throw new Error(
                     `Unknown year and day and part: ${yearDayPart}`
diff --git a/src/Solution/Strategy/2016/3/Common.ts b/src/Solution/Strategy/2016/3/Common.ts
new file mode 100644
index 0000000..4aeed88
--- /dev/null
+++ b/src/Solution/Strategy/2016/3/Common.ts
@@ -0,0 +1,17 @@
+export function sideLengthsAreValid(sideLengths: number[]) {
+    let possible = true;
+    for (let index = 0; index < sideLengths.length; index++) {
+        const val = sideLengths[index];
+        if (
+            val >=
+            [
+                ...sideLengths.slice(0, index),
+                ...sideLengths.slice(index + 1),
+            ].reduce((summed, toSum) => summed + toSum, 0)
+        ) {
+            possible = false;
+            break;
+        }
+    }
+    return possible;
+}
diff --git a/src/Solution/Strategy/2016/3/b.ts b/src/Solution/Strategy/2016/3/b.ts
new file mode 100644
index 0000000..6bea8e3
--- /dev/null
+++ b/src/Solution/Strategy/2016/3/b.ts
@@ -0,0 +1,56 @@
+import InterfaceSolutionStrategy from '../../../Interface/Strategy.js';
+import InterfaceInputFetcher from '../../../../InputFetcher/Interface/Service.js';
+import { sideLengthsAreValid } from './Common.js';
+import C1 from '../../../../Library/Input/Transformer/ColumnToRow/C1.js';
+/*
+--- Day 3: Squares With Three Sides ---
+Now that you can think clearly, you move deeper into the labyrinth of hallways and office furniture that makes up this part of Easter Bunny HQ. This must be a graphic design department; the walls are covered in specifications for triangles.
+
+Or are they?
+
+The design document gives the side lengths of each triangle it describes, but... 5 10 25? Some of these aren't triangles. You can't help but mark the impossible ones.
+
+In a valid triangle, the sum of any two sides must be larger than the remaining side. For example, the "triangle" given above is impossible, because 5 + 10 is not larger than 25.
+
+In your puzzle input, how many of the listed triangles are possible?
+*/
+
+class Solution20162a implements InterfaceSolutionStrategy {
+    inputFetcher: InterfaceInputFetcher;
+    constructor(inputFetcher: InterfaceInputFetcher) {
+        this.inputFetcher = inputFetcher;
+    }
+    async solve() {
+        const iterator = await this.inputFetcher.getAsyncIterator();
+        const transformer = new C1(3);
+        let possibleCount = 0;
+        // Keep track of how many rows we've proccessed
+        // Split input line into columns and verify size
+        // Use a multidimensional array to rearrange
+        // Flush to solver function
+        for await (let line of iterator) {
+            const columner = line.trim().split(/\s+/);
+            transformer.write(columner);
+            for (const sideLengthsRaw of transformer.read()) {
+                const sideLengths = sideLengthsRaw.map((sideLength) => {
+                    const parsed = parseInt(sideLength, 10);
+                    if (Number.isNaN(parsed)) {
+                        throw new Error(
+                            `Unable to parse input number! Raw lengths: ${sideLengthsRaw}. Raw length: ${sideLength}. Parsed length: ${parsed}`
+                        );
+                    }
+                    return parsed;
+                });
+                // Use slice to extract the number at positioin
+                // Send the remaining to a (eventually memoized) function that sums all elems in array
+                // Compare sum to number
+                // Conditionally increment counter
+                if (sideLengthsAreValid(sideLengths)) {
+                    ++possibleCount;
+                }
+            }
+        }
+        return possibleCount.toString();
+    }
+}
+export default Solution20162a;