thzinc

sumEveryOther - Interview question of the week from rendezvous with cassidoo

This was a fun, light interview question this week.

Interview question of the week

This week’s question: Given a number, sum every second digit in that number.

Example:

> sumEveryOther(548915381)
> 26 // 4+9+5+8

> sumEveryOther(10)
> 0

> sumEveryOther(1010.11)
> 1 // 0+0+1

My solution

My first pass at answering this question was to just accomplish the thing quickly and see how well it performs.

#mocha

  

function sumEveryOther(num) {
  const str = num.toString().replace(/\D+/, "");
  let acc = 0;
  for (let i = 0; i < str.length; i++) {
    if (i % 2 === 0) continue;
    const c = str[i];
    acc += parseInt(c, 10);
  }

  return acc;
}


mocha.setup("bdd");
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe("Given the examples from the question", () => {
  const expectations = [
    { input: 548915381, output: 26, because: "4, 9, 5, and 8" },
    { input: 10, output: 0, because: "0" },
    { input: 1010.11, output: 1, because: "0, 0, and 1" },
  ];
  expectations.forEach(({ input, output, because }) => {
    it(`should return ${output} for the number ${input} because the expected digits are ${because}`, () => {
      sumEveryOther(input).should.equal(output);
    });
  });
});

mocha.run();

I figured there might be a faster way than using regular expressions and calling parseInt() for each qualifying digit, so I took another pass that made use of the UTF-8/ASCII values of the characters in the number string.

#mocha

  

const ZERO_CHAR_CODE = "0".charCodeAt(0);
const NINE_CHAR_CODE = "9".charCodeAt(0);
function sumEveryOther(num) {
  const str = num.toString();
  let acc = 0;
  let skip = true;
  for (let i = 0; i < str.length; i++) {
    const c = str.charCodeAt(i);
    if (c < ZERO_CHAR_CODE || NINE_CHAR_CODE < c) {
      continue;
    }

    if (skip) {
      skip = false;
      continue;
    }

    skip = true;
    acc += c - ZERO_CHAR_CODE;
  }

  return acc;
}


mocha.setup("bdd");
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe("Given the examples from the question", () => {
  const expectations = [
    { input: 548915381, output: 26, because: "4, 9, 5, and 8" },
    { input: 10, output: 0, because: "0" },
    { input: 1010.11, output: 1, because: "0, 0, and 1" },
  ];
  expectations.forEach(({ input, output, because }) => {
    it(`should return ${output} for the number ${input} because the expected digits are ${because}`, () => {
      sumEveryOther(input).should.equal(output);
    });
  });
});

mocha.run();

I tested both implementations and included a rudimentary performance test where the function is called 1,000,000 times with random numbers:

#mocha

  

mocha.setup("bdd");
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe("When comparing an optimized implementation to an unoptimized implementation", () => {
  const maxIterations = 1_000_000;
  const unoptimized = getUnoptimized();
  const optimized = getOptimized();

  const results = [];
  for (const subject of [unoptimized, optimized]) {
    const start = performance.now();
    for (let i = 0; i < maxIterations; i++) {
      const rand = (Math.random() * 1_000_000_000_000) / 1_000_000;
      subject(rand);
    }
    const end = performance.now();
    results.push(end - start);
  }
  const [unoptimizedElapsedTimeMs, optimizedElapsedTimeMs] = results;

  it("should be faster", () => {
    optimizedElapsedTimeMs.should.be.below(unoptimizedElapsedTimeMs);
  });
});

mocha.run();


// Glue...
function getUnoptimized(){
function sumEveryOther(num) {
  const str = num.toString().replace(/\D+/, "");
  let acc = 0;
  for (let i = 0; i < str.length; i++) {
    if (i % 2 === 0) continue;
    const c = str[i];
    acc += parseInt(c, 10);
  }

  return acc;
}

return sumEveryOther;}
function getOptimized(){
const ZERO_CHAR_CODE = "0".charCodeAt(0);
const NINE_CHAR_CODE = "9".charCodeAt(0);
function sumEveryOther(num) {
  const str = num.toString();
  let acc = 0;
  let skip = true;
  for (let i = 0; i < str.length; i++) {
    const c = str.charCodeAt(i);
    if (c < ZERO_CHAR_CODE || NINE_CHAR_CODE < c) {
      continue;
    }

    if (skip) {
      skip = false;
      continue;
    }

    skip = true;
    acc += c - ZERO_CHAR_CODE;
  }

  return acc;
}

return sumEveryOther;}

See also