Advent of PBT 2021 - Day 12 - Solution
Tags: challenge, testing, webdev, javascript
Published: 2021-12-12 09:33:40

---
title: Advent of PBT 2021 - Day 12 - Solution
published: true
description: Advent of Property Based Testing 2021 - Day 12 - Solution
tags: challenge,testing,webdev,javascript
//cover_image: https://direct_url_to_image.jpg
---

Our algorithm was: **validParentheses**.
[Go to the subject itself for more details](https://dev.to/dubzzz/advent-of-pbt-2021-day-12-351b)

CodeSandbox with a possible set of properties you may have come with: https://codesandbox.io/s/advent-of-pbt-day-12-solution-qxu3z?file=/src/index.spec.ts&previewwindow=tests

---

## Property 1: should accept any well-parenthesized expression

As building a well-parenthesized expression is mostly a matter of recursion, we can consider that we could easily come up with an arbitrary to do so.

> for any well-parenthesized expression
> it should accept it

Written with fast-check:

```ts
it("should accept any well-parenthesized expression", () => {
  fc.assert(
    fc.property(wellParenthesizedStringArbitrary, (expression) => {
      expect(validParentheses(expression)).toBe(true);
    })
  );
});
```

A well-parenthesized expression can be seen as something like:

```ts
type WellParenthesized = {
  type: "(" | "[" | "{";
  content: WellParenthesized[];
};
```

In other words a `type` of bracket and a content made of other well-parenthesized expressions.

For instance we can define:
```js
const wellParenthesized = {
  type: '(',
  content: [
    { type: '[', content: [] },
    { type: '{', content: [] },
  ]
}
// corresponds to: ([]{})
```

Given a well-parenthesized definition, we can compute its string representation with:
```ts
function wellParenthesizedToString(definition: WellParenthesized): string {
  const { type, content } = definition;
  const openingBracket = type;
  const closingBracket = type === "(" ? ")" : type === "[" ? "]" : "}";
  return `${openingBracket}${content
    .map((p) => wellParenthesizedToString(p))
    .join("")}${closingBracket}`;
}
```

Now that we defined most the the building blocks of `wellParenthesizedStringArbitrary` we can write it by relying on `fc.letrec`:

```ts
// Arbitrary building instances of WellParenthesized
const wellParenthesizedArbitrary = fc.letrec((tie) => ({
  parenthesized: fc.record<WellParenthesized>({
    type: fc.constantFrom("(", "[", "{"),
    // We use a oneof instead of a raw array to enforce the convergence towards a finite structure
    content: fc.oneof(
      { depthFactor: 1 },
      fc.constant([]),
      fc.array(tie("parenthesized") as fc.Arbitrary<WellParenthesized>)
    )
  })
})).parenthesized;

// Arbitrary building valid parenthesized expressions
const wellParenthesizedStringArbitrary = fc
  .array(wellParenthesizedArbitrary)
  .map((def) => def.map((p) => wellParenthesizedToString(p)).join(""));
```

## Property 2: should reject any expression not containing an even number of signs

There are multiple ways not to be valid and building an arbitrary building any of the possible invalid parenthesized expressions is not that simple.

Instead, we will consider case by case.

For instance, any expression containing an odd number of symbols is known to be an invalid expression.

> for expression containing an odd number of brackets
> it should reject it

Written with fast-check:

```ts
it("should reject any expression not containing an even number of signs", () => {
  fc.assert(
    fc.property(
      fc
        .tuple(
          fc.array(
            fc.tuple(
              fc.constantFrom("(", "[", "{", ")", "]", "}"),
              fc.constantFrom("(", "[", "{", ")", "]", "}")
            )
          ),
          fc.constantFrom("(", "[", "{", ")", "]", "}")
        )
        .chain(([evenNumParentheses, extraParenthesis]) => {
          const parentheses = [...evenNumParentheses.flat(), extraParenthesis];
          return fc
            .shuffledSubarray(parentheses, { minLength: parentheses.length })
            .map((parentheses) => parentheses.join(""));
        }),
      (invalidExpression) => {
        expect(validParentheses(invalidExpression)).toBe(false);
      }
    )
  );
});
```

We could also have written it with something simpler like:

```ts
it("should reject any expression not containing an even number of signs (2)", () => {
  fc.assert(
    fc.property(
      fc
        .array(fc.constantFrom("(", "[", "{", ")", "]", "}"), { minLength: 1 })
        .filter((parentheses) => parentheses.length % 2 === 1)
        .map((parentheses) => parentheses.join("")),
      (invalidExpression) => {
        expect(validParentheses(invalidExpression)).toBe(false);
      }
    )
  );
});
```

But this one will reject half of the generated values.

## Property 3: should reject any expression not having the same number of openings and closings

Another possible cause of rejections is an expression with mismatching number of openings and closings.

> for expression not containing the same number of openings as closings
> it should reject it

Written with fast-check:

```ts
it("should reject any expression not having the same number of openings and closings", () => {
  fc.assert(
    fc.property(
      wellParenthesizedStringArbitrary,
      fc.constantFrom("(", "[", "{", ")", "]", "}"),
      fc.nat().noShrink(),
      (expression, extra, seed) => {
        const position = seed % (expression.length + 1);
        const invalidExpression =
          expression.substring(0, position) +
          extra +
          expression.substring(position);
        expect(validParentheses(invalidExpression)).toBe(false);
      }
    )
  );
});
```

This property is actually a sub-case of the property before. Indeed, the expression above is made of an odd number of signs so it will already be covered by the property 2. Property 3 only gives more details for one specific sub-case.

## Property 4: should reject any expression with at least one reversed openings and closings

Other ways to build invalid parenthesized expressions need to be fine tune one by one as there are multiple ways to go wrong. A simple example of that problem is the following property:

> for expression containing at least one reversed set of parentheses
> it should reject it

Actually this property is partially wrong, as:
```js
const wellParenthesizedDefinition = {
  type: '(',
  content: [
    {
      type: '(',
      content: []
    },
  ]
};
// corresponds to: (())
```

And:

```js
const reversedParenthesizedDefinition = {
  type: '(',
  content: [
    {
      type: '(',
      content: [],
      reversed: true,
    },
  ],
  reversed: false,
};
// corresponds to: ()()
```

Result both into well-parenthesized expressions.

In other words, building an invalid parenthesized expression is not just a matter of reversing one set of parentheses. With some fine tuning we can reach a quite decent property based on the idea of reversed parenthesized expressions but with some safety nets added to prevent falling into cases like the one discussed above.

Written with fast-check:

```ts
it("should reject any expression with at least one reversed openings and closings", () => {
  fc.assert(
    fc.property(reversedParenthesizedStringArbitrary, (expression) => {
      expect(validParentheses(expression)).toBe(false);
    })
  );
});
```

With `reversedParenthesizedStringArbitrary`:
```ts
type ReversedParenthesized = {
  type: "(" | "[" | "{";
  content: ReversedParenthesized[];
  reversed: boolean;
};

function reversedParenthesizedToString(
  subDefinition: ReversedParenthesized
): string {
  const { type, content, reversed } = subDefinition;
  const matching = type === "(" ? ")" : type === "[" ? "]" : "}";
  const openingBracket = reversed ? matching : type;
  const closingBracket = reversed ? type : matching;
  return `${openingBracket}${content
    .map((p) => reversedParenthesizedToString(p))
    .join("")}${closingBracket}`;
}

function hasReversed(subDefinition: ReversedParenthesized): boolean {
  if (subDefinition.reversed) return true;
  return subDefinition.content.some(
    (p) => subDefinition.type !== p.type && hasReversed(p)
  );
}

const reversedParenthesizedArbitrary = fc.letrec((tie) => ({
  parenthesized: fc.record<ReversedParenthesized>({
    reversed: fc.boolean(),
    type: fc.constantFrom("(", "[", "{"),
    // We use a oneof instead of a raw array to enforce the convergence towards a finite structure
    content: fc.oneof(
      { depthFactor: 1 },
      fc.constant([]),
      fc.array(tie("parenthesized") as fc.Arbitrary<ReversedParenthesized>)
    )
  })
})).parenthesized;

const reversedParenthesizedStringArbitrary = fc
  .array(reversedParenthesizedArbitrary)
  .filter((def) => def.some((p) => hasReversed(p)))
  .map((def) => def.map((p) => reversedParenthesizedToString(p)).join(""));
```

## Property 5: should reject any expression with non-matching openings and closings

With the same drawbacks as the ones explained in property 4, we can define a property like:

> for expression containing at least one non-matching set of parentheses
> it should reject it

Written with fast-check:

```ts
it("should reject any expression with non-matching openings and closings", () => {
  fc.assert(
    fc.property(nonMatchingEndParenthesizedStringArbitrary, (expression) => {
      expect(validParentheses(expression)).toBe(false);
    })
  );
});
```

With `nonMatchingEndParenthesizedStringArbitrary`:
```ts
type NonMatchingEndParenthesized = {
  start: "(" | "[" | "{";
  end: ")" | "]" | "}";
  content: NonMatchingEndParenthesized[];
};

const nonMatchingEndParenthesizedArbitrary = fc.letrec((tie) => ({
  parenthesized: fc.record<NonMatchingEndParenthesized>({
    start: fc.constantFrom("(", "[", "{"),
    end: fc.constantFrom(")", "]", "}"),
    // We use a oneof instead of a raw array to enforce the convergence towards a finite structure
    content: fc.oneof(
      { depthFactor: 1 },
      fc.constant([]),
      fc.array(
        tie("parenthesized") as fc.Arbitrary<NonMatchingEndParenthesized>
      )
    )
  })
})).parenthesized;

function nonMatchingEndParenthesizedToString(
  definition: NonMatchingEndParenthesized
): string {
  return `${definition.start}${definition.content
    .map((p) => nonMatchingEndParenthesizedToString(p))
    .join("")}${definition.end}`;
}

function hasNonMatchingEnd(
  subDefinition: NonMatchingEndParenthesized
): boolean {
  const matchingEnd =
    subDefinition.start === "(" ? ")" : subDefinition.start === "[" ? "]" : "}";
  if (subDefinition.end !== matchingEnd) return true;
  if (subDefinition.content.length !== 1)
    return subDefinition.content.some((p) => hasNonMatchingEnd(p));
  return false; // We still reject too many things
}

const nonMatchingEndParenthesizedStringArbitrary = fc
  .array(nonMatchingEndParenthesizedArbitrary)
  .filter((def) => def.some((p) => hasNonMatchingEnd(p)))
  .map((def) => def.map((p) => nonMatchingEndParenthesizedToString(p)).join(""));
```

---

[Back to "Advent of PBT 2021"](https://dev.to/dubzzz/advent-of-pbt-2021-13ee) to see topics covered during the other days and their solutions.

More about this serie on [@ndubien](https://twitter.com/ndubien) or with the hashtag [#AdventOfPBT](https://twitter.com/search?q=%23AdventOfPBT).