1
\$\begingroup\$

I wanted to practice functional programming (fp) without using any library but using vanilla JS only. So I took a problem from Advent of Code (the 2nd part of Day 2):

https://adventofcode.com/2017/day/2

I'm doing the 2nd part of Day 2. You can only access the 2nd part once you solved the 1st part.

To access the 2nd part type in this number 50376 or check out the solution here: Advent of Code Day 2 (1) in Functional programming (FP)

It sounds like the goal is to find the only two numbers in each row where one evenly divides the other - that is, where the result of the division operation is a whole number. They would like you to find those numbers on each line, divide them, and add up each line's result.

For example, given the following spreadsheet:

  • 5 9 2 8
  • 9 4 7 3
  • 3 8 6 5

In the first row, the only two numbers that evenly divide are 8 and 2; the result of this division is 4. In the second row, the two numbers are 9 and 3; the result is 3. In the third row, the result is 2. In this example, the sum of the results would be 4 + 3 + 2 = 9.

My solution in FP:

/*jshint esversion: 6*/
{
  'use strict';

  const INPUT =
    `6046   6349    208 276 4643    1085    1539    4986    7006    5374    252 4751    226 6757    7495    2923
1432    1538    1761    1658    104 826 806 109 939 886 1497    280 1412    127 1651    156
244 1048    133 232 226 1072    883 1045    1130    252 1038    1022    471 70  1222    957
87  172 93  73  67  192 249 239 155 23  189 106 55  174 181 116
5871    204 6466    6437    5716    232 1513    7079    6140    268 350 6264    6420    3904    272 5565
1093    838 90  1447    1224    744 1551    59  328 1575    1544    1360    71  1583    75  370
213 166 7601    6261    247 210 4809    6201    6690    6816    7776    2522    5618    580 2236    3598
92  168 96  132 196 157 116 94  253 128 60  167 192 156 76  148
187 111 141 143 45  132 140 402 134 227 342 276 449 148 170 348
1894    1298    1531    1354    1801    974 85  93  1712    130 1705    110 314 107 449 350
1662    1529    784 1704    1187    83  422 146 147 1869    1941    110 525 1293    158 1752
162 1135    3278    1149    3546    3686    182 149 119 1755    3656    2126    244 3347    157 865
2049    6396    4111    6702    251 669 1491    245 210 4314    6265    694 5131    228 6195    6090
458 448 324 235 69  79  94  78  515 68  380 64  440 508 503 452
198 216 5700    4212    2370    143 5140    190 4934    539 5054    3707    6121    5211    549 2790
3021    3407    218 1043    449 214 1594    3244    3097    286 114 223 1214    3102    257 3345`;

  const sum = (a, b) => a + b;
  const evenlyDiv = (diff, val) => {
    const row = val.split(/\t/);
    return diff.concat(row
      .reduce((d, v, i, line) => {
        return d + line.reduce((reduceRes, runningVar) => {
          return (runningVar % v === 0 && runningVar !== v) ?
            reduceRes + (runningVar / v) :
            reduceRes;
        }, 0);
      }, 0));
  };
  const solution = INPUT.split(/\n/)
    .reduce(evenlyDiv, [])
    .reduce(sum);

  console.log("solution ", solution);
}

Is there a better way to write it in FP with pure JavaScript, i.e. no additional FP library? Any improvement suggestions are welcomed.

\$\endgroup\$
  • \$\begingroup\$ @JerryCoffin: That's because I'm doing the 2nd part (as I clearly stated in the headline as well as in the description) of Day 2. You can only access the 2nd part once you solved the 1st part. To access the 2nd part type in this number 50376 or check out the solution here: codereview.stackexchange.com/questions/184477/… \$\endgroup\$ – thadeuszlay Jan 6 '18 at 19:38
  • \$\begingroup\$ @JerryCoffin done. Thanks for the suggestion. \$\endgroup\$ – thadeuszlay Jan 6 '18 at 19:41
1
\$\begingroup\$

While your code works, it took me a few tries to really understand how it works. I believe this is primarily caused by the deep nesting of evenlyDiv. FP is great for reusable code, which makes helper functions a really good idea. In this case, by using a few helpers you can make the code more readable.

A few pointers on your current solution:

  1. Don't use strings as numbers. Sure, it might work right now because division can't concatenate strings like addition can, but it is a dangerous habit to get into.
  2. There is no need to use a regular expression in this case to split the input into rows and columns. .split(/\n/) is the same as .split('\n').
  3. (d, v, i, line) is a lot of parameters, in this case you don't need two of them. Instead of line, you can just use row, and you never used i anyways.
  4. Don't default to reduce, the original function can be easily simplified by using map instead and just dropping the diff parameter.
  5. Try to avoid meaningless variable names. For very simple functions, it is fine to just use a, and b, but for anything longer than a line (and even some functions of only one line) it makes the code much more difficult to scan when another programmer in 6 months (you) looks at it.

Here is how I would implement the solution (assuming a relatively small table, if the table is very large, some optimization would be a good idea).

const INPUT = `5\t9\t2\t8
    9\t4\t7\t3
    3\t8\t6\t5`;

// Contained in most FP libraries, but simple to write
const unary = fn => arg => fn(arg)

const sum = (a, b) => a + b
const isNot = a => b => a !== b
const isDivisible = dividend => divisor => dividend % divisor === 0;

/**
 * Takes an array of numbers, returns the result of the division
 * of the first two numbers which divide evenly or 0 if no two 
 * numbers are divisible.
 */
const divideEvenly = numbers => {
  return numbers.reduce((carry, n) => {
    if (carry) return carry;

    const divisor = numbers
      .filter(isNot(n))
      .find(isDivisible(n));
    return divisor == null ? carry : n / divisor;
  }, 0);
}

// Without unary, parseInt would fail due to trying to parse with different bases
const solution = INPUT.split('\n')
  .map(line => line.split('\t').map(unary(parseInt)))
  .map(divideEvenly)
  .reduce(sum, 0);

console.log("solution ", solution);

\$\endgroup\$
  • \$\begingroup\$ "There is no need to use a regular expression" - why would you discourage from using regex? Are they slower or less declarative? \$\endgroup\$ – thadeuszlay Jan 7 '18 at 13:57
  • \$\begingroup\$ @thadeuszlay Regex is generally slower, yes, but I wouldn't worry about that here. The reason I recommend just using a string is that regex is such a powerful tool that really isn't necessary here. If you needed to match multiple combinations, using regex is absolutely the way to go, but for just a single character, or a simple sequence of characters, it is easier to just use a string. It is kind of like importing jQuery just to use $('p').remove() once in your code. \$\endgroup\$ – Gerrit0 Jan 7 '18 at 19:17
  • \$\begingroup\$ I don't get why the unary function is needed. I know that without it wouldn't work properly. But this works without the unary function const parse = line => line.split('\t') .map(toInt); \$\endgroup\$ – thadeuszlay Jan 13 '18 at 10:22
  • \$\begingroup\$ You can write everything without the unary function, true but it often results in cleaner code as you can avoid functions whose sole purpose is to call another function with one argument. \$\endgroup\$ – Gerrit0 Jan 13 '18 at 17:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.