JM's Blog

Codility Lesson 01 - Iterations - BinaryGap

Difficulty: [easy] Practice it here

BinaryGap

Find longest sequence of zeros in binary representation of an integer.

A binary gap within a positive integer N is any maximal sequence of consecutive zeros that is surrounded by ones at both ends in the binary representation of N.

For example,

Write a function:

function solution(N);

that, given a positive integer N, returns the length of its longest binary gap.

The function should return 0 if N doesn't contain a binary gap.

For example,

Write an efficient algorithm for the following assumptions:

N is an integer within the range [1..2,147,483,647].


Solution

BinaryGap

Find longest sequence of zeros in binary representation of an integer.

A binary gap within a positive integer N is any maximal sequence of consecutive zeros that is surrounded by ones at both ends in the binary representation of N.

For example,

Write a function:

function solution(N);

that, given a positive integer N, returns the length of its longest binary gap.

The function should return 0 if N doesn't contain a binary gap.

For example,

Write an efficient algorithm for the following assumptions:

N is an integer within the range [1..2,147,483,647].


Logical Thinking in Programming

Before starting to write a solution for any problem it is necessary to better understand it first. Many junior developers or people new to code fail in this step. The urge to start writing code is strong. I found the quotes below (don't ask me about the historical accuracy, focus on the idea instead):

"If I had an hour to solve a problem I’d spend 55 minutes thinking about the problem and 5 minutes thinking about solutions" - Albert Einstein

"Give me six hours to chop down a tree and I will spend the first four sharpening the axe" - Abraham Lincoln

"A problem well defined is a problem half solved" - Dan Martell

Logical thinking involves using systematic reasoning to solve problems efficiently. In programming, this means applying clear, structured thought processes to debugging, algorithm optimization, and feature design tasks. The primary methods include deductive reasoning, inductive reasoning, and problem decomposition.

With that in mind, let's take a look at our first problem.

The Problem/Solution Thinking

Based on the problem description: I will assume the input is "positive integer N", and that the parameter N passed to the solution(N) is decimal. Then in order to find a binary gap in a given number, I need to convert it from decimal to binary.

To understand it and make things more clear, I created the table below. I like to visualize and look for patterns before writing the code.

Each decimal number has its binary representation and now I can see the '0' gaps. Some numbers have more than one gap and some have none.

+--------+-----------------------+------------+--------+
| Number | Binary Representation | Binary Gap | Result |
+--------+-----------------------+------------+--------+
|      9 |                  1001 | 2          |      2 |
|    529 |            1000010001 | 4 & 3      |      4 |
|     20 |                 10100 | 1          |      1 |
|     15 |                  1111 | 0          |      0 |
|     32 |                100000 | 0          |      0 |
|   1041 |           10000010001 | 5 & 3      |      5 |
+--------+-----------------------+------------+--------+

Write Some Code

Before jumping into the solution, I need to create my code and test files. So I copied the template files solution-template.js and solution-template.test.js in the "01-Iterations" folder.

The first file is the code (solution) itself and it was renamed from solution-template.js to binary-gap.js:

// binary-gap.js
'use strict';

module.exports = function solution(N) {
  var maxGap = -1;

  return maxGap;
}

I will return -1 as an invalid value for now.

The other file is the unit test suite for this problem, which was renamed from solution-template.test.js to binary-gap.test.js:

// binary-gap.test.js
'use strict';

const solution = require("./binary-gap");

describe('BinaryGap Tests', () => {

  test('given 1 should return 1.', () => {
    expect(solution(1)).toBe(1);
  });

});

Now I can start writing unit tests and solution code.

Adding Unit Tests First

TDD is called Test Driven Development, which means the development is driven by tests.

The process follows a cycle called "Red-Green-Refactor": write a failing test (Red), write just enough code for the test to pass (Green), and then clean up the code while ensuring tests remain green (Refactor). This iterative approach helps create higher-quality, more maintainable code by ensuring every new piece of functionality is built upon a passing test.

Based on the instructions provided in the problem description, I can start adding the test cases.

Feel free to add more, start simple and add more as you go and look for edge cases or potential issues. It is good to also add test cases to check conditions outside the scope or boundaries too.

Start with the requirements from the problem description:

The test syntax is pretty straightforward, but in doubt check the Jest documentation.

The problem description does not say anything about errors, so I will assume that an invalid parameter will throw an error.

The first tests will cover boundaries and invalid numbers from this statement in the problem description:

N is an integer within the range [1 .. 2,147,483,647].

// binary-gap.test.js
'use strict';

const solution = require("./binary-gap");

describe('BinaryGap Tests', () => {

  // valid input: negative number
  test('Given -1 should throw an error.', () => {
    expect(() => {
      solution(-1);
    }).toThrow();
  });

  // valid input: zero
  test('Given 0 should throw an error.', () => {
    expect(() => {
      solution(0);
    }).toThrow();
  });

  // valid input: big number
  test('Given 2,147,483,648 should throw an error.', () => {
    expect(() => {
      solution(2147483648);
    }).toThrow();
  });

  // valid input boundary: 1
  test('Given 1 [0001] should return 0.', () => {
    expect(solution(1)).toBe(0);
  });

  // valid input boundary: 2,147,483,647
  test('Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.', () => {
    expect(solution(2147483647)).toBe(0);
  });

After adding the test cases to binary-gap.test.js, run the tests with:

npm test

or using the "watch mode" so the tests will run automatically and also using "verbose" to display more information about the test cases being executed:

npm run test -- --watch --verbose

And the test result is:

 PASS  00-template/solution-template.test.js
  Template Tests
    √ given -1 should throw an error (9 ms)
    √ given 1 should return 1. (1 ms)

 FAIL  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    × Given -1 should throw an error. (4 ms)
    × Given 0 should throw an error.
    × Given 2,147,483,648 should throw an error.
    × Given 1 [0001] should return 0. (1 ms)
    × Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.

Test Suites: 1 failed, 1 passed, 2 total
Tests:       5 failed, 2 passed, 7 total
Snapshots:   0 total
Time:    0.732 s, estimated 1 s

The solution-template.tests.js has 1 test suite and 2 unit tests - all passing.

The binary-gap.tests.js has 1 test suite and 5 unit tests - all failing.

A total of 2 test suites and 7 unit tests.

With the validations and boundaries in place, I can add more tests using the requirements in the problem description. Lets check some other numbers. These will be valid input numbers that should return valid output results. For example: 9, 529, 20, 15, 32, 1041.

In the test name, I added the converted binary number inside the backets to make it easier to understand: 9 decimal = [1001] binary.

Like this: 'Given 9 [1001] should return 2.'

Here are the unit tests:

  // valid input boundary: 1
  test('Given 1 [0001] should return 0.', () => {
    expect(solution(1)).toBe(0);
  });

  // valid input boundary: 2,147,483,647
  test('Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.', () => {
    expect(solution(2147483647)).toBe(0);
  });

  // test numbers from the problem description:
  test('Given 9 [1001] should return 2.', () => {
    expect(solution(9)).toBe(2);
  });
  test('Given 529 [0010.0001.0001] should return 4.', () => {
    expect(solution(529)).toBe(4);
  });
  test('Given 20 [0001.0100] should return 1.', () => {
    expect(solution(20)).toBe(1);
  });
  test('Given 15 [1111] should return 0.', () => {
    expect(solution(15)).toBe(0);
  });
  test('Given 32 [0010.0000] should return 0.', () => {
    expect(solution(32)).toBe(0);
  });
  test('Given 1041 [0100.0001.0001] should return 5.', () => {
    expect(solution(1041)).toBe(5);
  });

Run the tests again, and some of the tests will fail. This is expected.

I removed part of the results because they are very similar and I will focus on fixing one error at a time. Here is the test output:

 PASS  00-template/solution-template.test.js
  Template Tests
    √ given -1 should throw an error (10 ms)
    √ given 1 should return 1.

 FAIL  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    × Given -1 should throw an error. (5 ms)
    × Given 0 should throw an error. (1 ms)
    × Given 2,147,483,648 should throw an error. (1 ms)
    × Given 1 [0001] should return 0. (2 ms)
    × Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0. (1 ms)
    × Given 9 [1001] should return 2.
    × Given 529 [0010.0001.0001] should return 4. (1 ms)
    × Given 20 [0001.0100] should return 1.
    × Given 15 [1111] should return 0.
    × Given 32 [0010.0000] should return 0.
    × Given 1041 [0100.0001.0001] should return 5. (1 ms)

... more errors ...

Test Suites: 1 failed, 1 passed, 2 total
Tests:       11 failed, 2 passed, 13 total
Snapshots:   0 total
Time:    0.725 s, estimated 1 s

Write Some Code

Now it is time to start writing some functional code.

Note that I don't have a "main" or "index" as an application entry point calling the solution(N) function. This is intended and if I run npm start I will get an error. I want to test my function in isolation, provide multiple inputs and evaluate the results. This would be more difficult and time consuming to do without a test framework.

With the tests in place, I can now start writing the code to solve the problem. I may even go back and add more tests for other uses cases, but this is enough for now. TDD is an incremental approach.

The first thing I usually do is to add some validations to test the input parameters in the solution(N) function inside the binary-gap.js file.

First, for this statement in the problem description:

N is an integer within the range [1 .. 2,147,483,647].

I already have the unit tests, now it is time to write the functional code to address that.

// `binary-gap.js`
'use strict';

module.exports = function solution(N) {
  var maxGap = -1;

  // validations
  if (N < 1 || N > 2147483647) {
    throw new Error("Parameter is invalid!");
  }

  return maxGap;
}

Then run the tests again with:

npm test

And he result is:

 FAIL  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    √ Given -1 should throw an error. (11 ms)
    √ Given 0 should throw an error. (1 ms)
    √ Given 2,147,483,648 should throw an error. (1 ms)
    × Given 1 [0001] should return 0. (2 ms)
    × Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.
    × Given 9 [1001] should return 2.
    × Given 529 [0010.0001.0001] should return 4. (1 ms)
    × Given 20 [0001.0100] should return 1. (1 ms)
    × Given 15 [1111] should return 0. (1 ms)
    × Given 32 [0010.0000] should return 0. (1 ms)
    × Given 1041 [0100.0001.0001] should return 5. (1 ms)

Now I have some progress and the first 3 tests as passing! They check for numbers outside the scope (invalid inputs).

Next, I will start the problem/solution logic.


The first thing to consider is that my input number is a decimal number. I need to convert it to its binary representation.

In JavaScript, the simplest and most common method to convert a decimal number to its binary representation is by using the built-in toString() method with a radix of 2.

See Javascript Number.prototype.toString() reference at MDN.

// `binary-gap.js`
'use strict';

module.exports = function solution(N) {
  var maxGap = -1;

  // validations
  if (N < 1 || N > 2147483647) {
    throw new Error("Parameter is invalid!");
  }

  // first, convert the number to binary
  bin = N.toString(2);
  console.log(bin);

  return maxGap;
}

And run the tests again.

Looking at the test results again, I can plan the next steps.

 FAIL  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    √ Given -1 should throw an error. (11 ms)
    √ Given 0 should throw an error. (1 ms)
    √ Given 2,147,483,648 should throw an error. (1 ms)
    × Given 1 [0001] should return 0. (2 ms)
    × Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.
    × Given 9 [1001] should return 2.
    × Given 529 [0010.0001.0001] should return 4. (1 ms)
    × Given 20 [0001.0100] should return 1. (1 ms)
    × Given 15 [1111] should return 0. (1 ms)
    × Given 32 [0010.0000] should return 0. (1 ms)
    × Given 1041 [0100.0001.0001] should return 5. (1 ms)

  ● BinaryGap Tests › Given 1 [0001] should return 0.

    expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: -1

      28 |   // valid input boundary: 1
      29 |   test('Given 1 [0001] should return 0.', () => {
    > 30 |     expect(solution(1)).toBe(0);
     |             ^
      31 |   });
      32 |
      33 |   // valid input boundary: 2,147,483,647

      at Object.toBe (01-Iterations/binary-gap.test.js:30:25)

The test results show which tests failed and some other important information:

BinaryGap Tests › Given 1 [0001] should return 0.

This tells me that:

In summary: the test received the input parameter "1" and expected the result to be "0" but received "-1" instead.

The output will show where the unit test failed, not the functional code (there is no code yet!). So the first thing to do is look at this condition and write the functional code to fix this test.

Problem Logic

Note that I added the console.log(bin); after the conversion. The reason for that is to see what the result of the decimal to binary conversion is. This is important because depending on the language or the result returned from the binary conversion function, I may need to remove the trailing zeros (they can be discarded).

dec bin bytes
1 1 0001
5 101 0101
7 111 0111
9 1001 1001
32 100000 0010.0000

Note:

Here is one situation where it is OK to do a Google search. I usually work with multiple languages and environments (C#, C, C++, Python, JavaScript, PowerShell, T-SQL, TypeScript just to name a few) and there is no way to know all the syntaxes and parameters for all languages and functions.

But there is a difference between asking "how to convert decimal to binary in JavaScript" to "give me the solution to this problem so I can copy it". Sometimes in a real work situation it is necessary to just get one solution and apply it, instead of reinventing the wheel and creating your own solution. Viewer discretion is advised.

I will remove the console.log(bin); now, because it affects performance. It is OK to have it while developing/debugging, but it is ideal to remove before shipping the code (when the work is done).

Assuming there are no trailing 0's so that all binary numbers will start with 1 (see table above, column 'bin') and the converted binary number can be accessed as an array, I can use the index/array syntax:

  N = 5 (decimal)
  bin = 101 (binary)
then
  bin[0] = 1
  bin[1] = 0
  bin[2] = 1

Algorithm:

Next, I will iterate over the binary digits and count the 0's. I added a new variable zeros = 0 to hold the count of 0's. After the loop, I need to assign maxGap = zeros; because I return maxGap.

    let maxGap = -1;
    let zeros = 0;
    // now iterate over the 0's and 1's and count
    for (var x = 0; x < bin.length; x++) {
    if (bin[x] == 0) {
        zeros++;
    }
    }
    maxGap = zeros;
    return maxGap;

Run the tests again:

 FAIL  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    √ Given -1 should throw an error. (10 ms)
    √ Given 0 should throw an error.
    √ Given 2,147,483,648 should throw an error.
    √ Given 1 [0001] should return 0. (1 ms)
    √ Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.
    √ Given 9 [1001] should return 2.
    × Given 529 [0010.0001.0001] should return 4. (4 ms)
    × Given 20 [0001.0100] should return 1. (1 ms)
    √ Given 15 [1111] should return 0. (1 ms)
    × Given 32 [0010.0000] should return 0. (2 ms)
    × Given 1041 [0100.0001.0001] should return 5. (1 ms)

  ● BinaryGap Tests › Given 529 [0010.0001.0001] should return 4.

This is interesting, the failed tests tell me that the returned value is wrong when I have more than one gap: 529 [0010.0001.0001] and 20 [0001.0100] or when there is no 1 closing the last gap [100000].

To fix that I will need to modify my main loop and add some more checks. I need to check for 1's too (closing gap).

    // now iterate over the 0's and 1's and count
    for (var x = 0; x < bin.length; x++) {
    if (bin[x] == 0) { // means that there is gap
        zeros++;
    }
    if (bin[x] == 1) { // means that the gap ended
        // if the gap ended, check if this is biggest found
        if (zeros > maxGap) {
        maxGap = zeros;
        }
        // reset the zero counter
        zeros = 0;
    }
    }
    return maxGap;

Run the tests again:

 PASS  00-template/solution-template.test.js
  Template Tests
    √ given -1 should throw an error (10 ms)
    √ given 1 should return 1.

 PASS  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    √ Given -1 should throw an error. (11 ms)
    √ Given 0 should throw an error. (1 ms)
    √ Given 2,147,483,648 should throw an error.
    √ Given 1 [0001] should return 0. (1 ms)
    √ Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.
    √ Given 9 [1001] should return 2. (1 ms)
    √ Given 529 [0010.0001.0001] should return 4.
    √ Given 20 [0001.0100] should return 1. (1 ms)
    √ Given 15 [1111] should return 0.
    √ Given 32 [0010.0000] should return 0. (1 ms)
    √ Given 1041 [0100.0001.0001] should return 5.

Test Suites: 2 passed, 2 total
Tests:       13 passed, 13 total
Snapshots:   0 total
Time:    0.854 s, estimated 1 s

And we have a solution!

Now I can start improving the solution, cleaning up the code, adding more tests.

Just to be sure, I went back to the Codility site and tested my solution there. The site also show some other unit tests, so I added them to my test suite too.

And ran the tests again.

 PASS  00-template/solution-template.test.js
  Template Tests
    √ given -1 should throw an error (10 ms)
    √ given 1 should return 1. (1 ms)

 PASS  01-Iterations/binary-gap.test.js
  BinaryGap Tests
    √ Given -1 should throw an error. (13 ms)
    √ Given 0 should throw an error. (1 ms)
    √ Given 2,147,483,648 should throw an error. (1 ms)
    √ Given 1 [0001] should return 0.
    √ Given 2147483647 [0111.1111.1111.1111.1111.1111.1111.1111] should return 0.
    √ Given 9 [1001] should return 2.
    √ Given 529 [0010.0001.0001] should return 4. (1 ms)
    √ Given 20 [0001.0100] should return 1.
    √ Given 15 [1111] should return 0.
    √ Given 32 [0010.0000] should return 0.
    √ Given 1041 [0100.0001.0001] should return 5.
    √ Given 64 [0100.0000] should return 0. (1 ms)
    √ Given 132 [1000.0100] should return 4.
    √ Given 133 [1000.0101] should return 4.
    √ Given 5 [0101] should return 1.
    √ Given 6 [0110] should return 0.
    √ Given 9 [1001] should return 2.
    √ Given 11 [1011] should return 1. (1 ms)
    √ Given 16 [0001.0000] should return 0.
    √ Given 19 [0001.0011] should return 2. (1 ms)
    √ Given 42 [0010.1010] should return 1.
    √ Given 328 [0001.0100.1000] should return 2. (1 ms)
    √ Given 1024 [0100.0000.0000] should return 0.
    √ Given 1162 [0100.1000.1010] should return 3.
    √ Given 51712 [0110.0101.0000.0000] should return 2. (1 ms)
    √ Given 561892 [1000.1001.0010.1110.0100] should return 3.
    √ Given 66561 [0001.0000.0100.0000.0001] should return 9.
    √ Given 6291457 [0110.0000.0000.0000.0000.0001] should return 20. (1 ms)
    √ Given 74901729 [0100.0111.0110.1110.1000.1110.0001] should return 4.
    √ Given 805306373 [0011.0000.0000.0000.0000.0000.0000.0101] should return 25.
    √ Given 1376796946 [0101.0010.0001.0000.0100.0001.0001.0010] should return 5.
    √ Given 1073741825 [0100.0000.0000.0000.0000.0000.0000.0001] should return 29. (1 ms)
    √ Given 1610612737 [0110.0000.0000.0000.0000.0000.0000.0001] should return 28.

Test Suites: 2 passed, 2 total
Tests:       35 passed, 35 total
Snapshots:   0 total
Time:    0.882 s, estimated 1 s
Ran all test suites related to changed files

Suggestions

Sometimes, when working on a problem it is better to use Pseudocode to think about the algorithm logic, this way I don't have to worry about the syntax or language construction rules.

Something like this:

  let maxGap = 0; // max gap found
  let zeros = 0; // zero counter

  // convert input N to bin
  let n = ConvertToBinary(N);

  // example:
  // N = 5 (decimal) => n = 101 (binary)
  // so
  // n[0] = 1
  // n[1] = 0
  // n[2] = 1

  // loop through all binary digits
  for x = 0 to n.length:
    if x == 0 then
      // n[0] = 1 first digit
      skip // do nothing
    end if
    if x > 0 then
      // second digit and on
      // check if is 0
      if n[x] == 0 then // it is a 0 gap
    // increase out counter
    zeros++
      end if
      // save the maxGap number
      maxGap = zeros
    end if
  next x
  return maxGap

Then it is easier later to find the correct syntax for the language I am using.