Javascript refactoring

Lately, my friend was applying for a Javascript Developer position. He was assigned a refactoring task and asked me for advice. So I helped him and decided to post a full solution because it's a very good piece of code to practice.

Current (legacy) code

The current solution was created in Javascript. It consists of a single EventParser class and two methods makeEventName and formatScore.

The result of this app should be an array of objects with two attributes: name and score.

The input data is an array of objects (matches). The valid input object should have four keys: sport, participant1, participant2 and score. These keys can be invalid or missing. For better readability, the invalid entries are marked with comments, i.e. missing score.

Next, this data is processed in a for loop and the result is generated.

As we can see this solution is not flexible, not extendable and prone to errors. Here's the source code of the legacy solution:

class EventParser {
    makeEventName(match) {
        if (match.sport === 'soccer') {
            return match.participant1 + ' - ' + match.participant2;
        } else if (match.sport === 'tennis') {
            return match.participant1 + ' vs ' + match.participant2;
        } else if (match.sport === 'volleyball') {
            return match.participant1 + ' - ' + match.participant2;
        } else if (match.sport === 'handball') {
            return match.participant1 + ' vs ' + match.participant2;
        } else if (match.sport === 'basketball') {
            return match.participant1 + ' - ' + match.participant2;
        } else {
            return 'Exception: invalid sport';
        }
    }

    formatScore(match) {
        if (match.sport === 'soccer') {
            return match.score;
        } else if (match.sport === 'tennis') {
            var scores = /([0-9]+\:[0-9]+),([0-9]+\:[0-9]+),([0-9]+\:[0-9]+),([0-9]+\:[0-9]+)/.exec(match.score);
            var set1 = scores[2];
            var set2 = scores[3];
            var set3 = scores[4];

            return 'Main score: ' + scores[1] + ' (' + 'set1 ' + set1 + ', ' + 'set2 ' + set2 + ', ' + 'set3 ' + set3 + ')';
        } else if (match.sport === 'volleyball') {
            var scores = /([0-9]+\:[0-9]+),([0-9]+\:[0-9]+),([0-9]+\:[0-9]+),([0-9]+\:[0-9]+)/.exec(match.score);
            var set1 = scores[2];
            var set2 = scores[3];
            var set3 = scores[4];

            return 'Main score: ' + scores[1] + ' (' + 'set1 ' + set1 + ', ' + 'set2 ' + set2 + ', ' + 'set3 ' + set3 + ')';
        } else if (match.sport === 'basketball') {
            return match.score[0][0] + ',' + match.score[0][1] + ',' + match.score[1][0] + ',' + match.score[1][1];
        } else if (match.sport === 'handball') {
            return match.score;
        } else {
            return 'Exception: invalid sport';
        }
    }
}

var matches = [
    {
        sport: 'soccer',
        participant1: 'Chelsea',
        participant2: 'Arsenal',
        score: '2:1',
    },
    {
        sport: 'volleyball',
        participant1: 'Germany',
        participant2: 'France',
        score: '3:0,25:23,25:19,25:21',
    },
    {
        sport: 'handball',
        participant1: 'Pogoń Szczeciń',
        participant2: 'Azoty Puławy',
        score: '34:26',
    },
    {
        sport: 'basketball',
        participant1: 'GKS Tychy',
        participant2: 'GKS Katowice',
        score: [
            ['9:7', '2:1'],
            ['5:3', '9:9'],
        ],
    },
    {
        sport: 'tennis',
        participant1: 'Maria Sharapova',
        participant2: 'Serena Williams',
        score: '2:1,7:6,6:3,6:7',
    },
    {
        sport: 'soccer',
        // missing score
        participant1: 'Chelsea',
        participant2: 'Arsenal',
    },
    {
        // missing sport name
        score: '2:3',
        participant1: 'Chelsea',
        participant2: 'Arsenal',
    },
    {
        sport: 'tennis',
        score: '2:1,7:6,6:3,6:7',
        participant1: 'Maria Sharapova',
        // missing participant2
    },
    {
        sport: 'tennis',
        score: '2:1,7:6,6:3,6:7',
        // missing participant1
        participant2: 'Serena Williams',
    },
    {
        sport: 'hockey', // not existing sport
        participant1: 'Maria Sharapova',
        participant2: 'Serena Williams',
        score: '2:1,7:6,6:3,6:7',
    },
    {
        sport: 'tennis',
        participant1: 'Maria Sharapova',
        participant2: 'Serena Williams',
        score: '0-1', // invalid score format
    },
];


let matchesParsed = [];

for (var i = 0; i < matches.length; i++) {
    let parser = new EventParser();
    let name = parser.makeEventName(matches[i]);
    let score = parser.formatScore(matches[i]);

    if (name !== 'Exception: invalid sport' && score !== 'Exception: invalid sport') {
        matchesParsed.push({
            name,
            score,
        });
    }
}

console.log(matchesParsed);

Solution

Dependencies

Initialize npm with default settings.

npm init -y

Install required dependencies. For testing: jest and ts-jest. For compilation: typescript.

npm i jest@28 @types/jest@28 ts-jest@28 typescript@4.7

Initialize the default configuration of ts-jest to run tests created in typescript.

npx ts-jest config:init

Implementation

Basic data structures

All supported types of sports will be held in the Sport enum.

export enum Sport {
  soccer = 'soccer',
  volleyball = 'volleyball',
  handball = 'handball',
  basketball = 'basketball',
  tennis = 'tennis',
}

Create a Match interface responsible for the input data.

export interface Match {
  sport: string;
  participant1: string;
  participant2: string;
  score: string | Array<string[]>
}

Create a SportEvent interface responsible for the output data.

export interface SportEvent {
  name: string;
  score: string;
}

Create EventParserException abstract class which will be a base class for all exceptions thrown by our application. Now, we can easily distinguish our app's exceptions from external exceptions.

export abstract class EventParserException extends Error {

}

Input data processing

Create EventParser class responsible for taking input data and returning parsed MatchEvent.

export class EventParser {
  parse(match: Partial<Match>): SportEvent {

  }
}

The partial match class (Partial< Match >) is used due to the possibility of missing any key in the Match interface.

The base class EventParser from the legacy solution has two methods makeEventName and formatScore. This violates the Single Responsibility principle and that's why these functionalities will be delegated to separate classes.

Name event handling

Create EventNameMaker class skeleton.

export class EventNameMaker {
  makeName(match: Partial<Match>): string {

  }
}

Add participant1 and participant2 keys validation. The method validateParticipant takes the participant's name and throws an InvalidParticipantNameException exception if the name is invalid.

export class EventNameMaker {
  makeName(match: Partial<Match>): string {
    this.validateParticipant(match.participant1);
    this.validateParticipant(match.participant2);
  }

  private validateParticipant(name: string | undefined): void {
    if (typeof name !== 'string') {
      throw new InvalidParticipantNameException();
    }

    if (name.trim().length === 0) {
      throw new InvalidParticipantNameException();
    }
  }
}

Add class InvalidParticipantNameException.

export class InvalidParticipantNameException extends EventParserException {
  message = 'Invalid participant name';
}

Support for generating event names for each type of sport.

The EventNameMaker class itself, should not be responsible for generating any event name, because it's not its responsibility. This responsibility will be delegated to separate classes implementing the MakeNameInterface interface.

export interface MakeNameInterface {
  makeName(participant1: string, participant2: string): string;
}

Legacy code analysis shows, that there are only two types of name formatting:

  • participant names separated with "vs" word

  • participant names separated with "dash" symbol

Create two classes implementing the MakeNameInterface interface.

export class VsSeparatedNameMaker implements MakeNameInterface {
  makeName(participant1: string, participant2: string): string {
    return `${ participant1 } vs ${ participant2 }`;
  }
}
export class DashSeparatedNameMaker implements MakeNameInterface {
  makeName(participant1: string, participant2: string): string {
    return `${ participant1 } - ${ participant2 }`;
  }
}

Add a constructor to EventNameMaker class with two previously created dependencies.

export class EventNameMaker {
  constructor(
    private readonly vsSeparatedNameMaker: VsSeparatedNameMaker,
    private readonly dashSeparatedNameMaker: DashSeparatedNameMaker,
  ) {
  }
}

Use the switch statement to handle sports types and make use of injected classes.

export class EventNameMaker {
  makeName(match: Partial<Match>): string {
    this.validateParticipant(match.participant1);
    this.validateParticipant(match.participant2);

    switch (match.sport) {
      case Sport.soccer:
      case Sport.volleyball:
      case Sport.basketball: {
        return this.dashSeparatedNameMaker.makeName(match.participant1, match.participant2);
      }
      case Sport.handball:
      case Sport.tennis: {
        return this.vsSeparatedNameMaker.makeName(match.participant1, match.participant2);
      }
      default:
        throw new UnsupportedSportException();
    }
  }
}

When the app encounters an unsupported or invalid sport type it throws an UnsupportedSportException exception.

export class UnsupportedSportException extends EventParserException {
  message = 'Unsupported sport';
}

Parsing the match results

Create EventScoreFormatter class skeleton.

export class EventScoreFormatter {
  formatScore(match: Partial<Match>): string {

  }
}

Create initial validation of score key. To do so, create a validateScore method that takes a single score and checks if the value is even set. If not the InvalidScoreException exception is thrown.

export class EventScoreFormatter {
  formatScore(match: Partial<Match>): string {
    this.validateScore(match.score);
  }

  private validateScore(score: unknown): void {
    if (score == null) {
      throw new InvalidScoreException();
    }
  }
}
export class InvalidScoreException extends EventParserException {
  message = 'Invalid score';
}

Prepare support for formatting results for each type of sport. Each one of them should be handled by a separate class. To do so, create the FormatScoreInterface interface.

interface FormatScoreInterface {
  formatScore(score: unknown): string;
}

Legacy code analysis shows, that there are only three types of results formatting. Therefore, create three classes implementing the FormatScoreInterface interface. Also, every single result is formatted the same way: two positive integers separated by a colon, i.e.:

9:14

Create MatchScoreParser class that validates and formats a single result.

First, check with Regular Expressions whether a given result matches a pattern. Then we convert it to the Number type and return it as a MatchScore object.




export class MatchScoreParser {
  private readonly scoreRegexp = new RegExp('^([0-9]+):([0-9]+)$');

  parse(score: string): MatchScore {
    const regexpMatch = this.scoreRegexp.exec(score);
    if (!regexpMatch) {
      throw new InvalidScoreFormatException();
    }

    return {
      participant1Score: Number.parseInt(regexpMatch[1]),
      participant2Score: Number.parseInt(regexpMatch[2]),
    }
  }
}
export class InvalidScoreFormatException extends EventParserException {
  message = 'Invalid score format';
}
export interface MatchScore {
  participant1Score: number;
  participant2Score: number;
}

Implement concrete classes responsible for parsing the results of each type of sport.

Create a SingleMatchScoreFormatter class that parses single-match result. This class will be used in the case of soccer and handball sports.

export class SingleMatchScoreFormatter implements FormatScoreInterface {
  constructor(
    private readonly matchScoreParser: MatchScoreParser
  ) {
  }

  formatScore(score: unknown): string {
    if (typeof score !== 'string') {
      throw new InvalidScoreFormatException();
    }

    const matchScore = this.matchScoreParser.parse(score);

    return `${ matchScore.participant1Score }:${ matchScore.participant2Score }`;
  }
}

In the same fashion prepare tennis and volleyball sport's results handler (MultiSetScoreFormatter). To parse a single result use the previously created SingleMatchScoreFormatter class.

export class MultiSetScoreFormatter implements FormatScoreInterface {
  constructor(
    private readonly singleMatchScoreFormatter: SingleMatchScoreFormatter
  ) {
  }

  formatScore(score: unknown): string {
    this.validateScore(score);
    const matchScores = this.extractMatchScores(score as string);
    const mainScore = matchScores.shift();
    const formattedMatchScores = matchScores.map((matchScore, index) => {
      return `set${ index + 1 } ${ matchScore }`;
    }).join(', ');

    return `Main score: ${ mainScore } (${ formattedMatchScores })`;
  }

  private extractMatchScores(score: string): string[] {
    const matchResults = score.split(',');
    if (matchResults.length !== 4) {
      throw new InvalidScoreFormatException();
    }

    return matchResults.map(mr => this.singleMatchScoreFormatter.formatScore(mr));
  }

  private validateScore(score: unknown): void {
    if (typeof score !== 'string') {
      throw new InvalidScoreFormatException();
    }
  }
}

The formatScore method works as follows:

  • validate input data syntax
this.validateScore(score);
  • extract individual match scores into an array
const matchScores = this.extractMatchScores(score as string);
  • separate the main result from the rest
const mainScore = matchScores.shift();
  • format the results
const formattedMatchScores = matchScores.map((matchScore, index) => {
  return `set${ index + 1 } ${ matchScore }`;
}).join(', ');
  • return a fully formatted result
return `Main score: ${ mainScore } (${ formattedMatchScores })`;

Implement result formatter for basketball sport. To parse a single result use the previously created SingleMatchScoreFormatter class.

export class MultiQuarterScoreFormatter implements FormatScoreInterface {
  constructor(
    private readonly singleMatchScoreFormatter: SingleMatchScoreFormatter
  ) {
  }

  formatScore(score: unknown): string {
    this.validateScore(score);
    const matchScores = this.extractMatchScores(score as string[][]);

    return matchScores.join(',');
  }

  private extractMatchScores(score: string[][]): string[] {
    return score.reduce((previous, current) => previous.concat(current), [])
      .map(mr => this.singleMatchScoreFormatter.formatScore(mr));
  }

  private validateScore(score: unknown): void {
    if (!(score instanceof Array)) {
      throw new InvalidScoreFormatException();
    }

    if (score.length !== 2) {
      throw new InvalidScoreFormatException();
    }

    score.forEach(s => {
      if (!(s instanceof Array)) {
        throw new InvalidScoreFormatException();
      }
    });
  }
}

The method formatScore works as follows:

  • validate the input data
    this.validateScore(score);
  • extract scores into an array with formatted scores
    const matchScores = this.extractMatchScores(score as string[][]);
  • return a fully formatted result
    return matchScores.join(',');

Add previously created classes in the constructor. Use a switch statement to handle each type of sport.

export class EventScoreFormatter {
  constructor(
    private readonly multiQuarterScoreFormatter: FormatScoreInterface,
    private readonly singleMatchScoreFormatter: FormatScoreInterface,
    private readonly multiSetScoreFormatter: FormatScoreInterface,
  ) {
  }

  formatScore(match: Partial<Match>): string {
    this.validateScore(match.score);

    switch (match.sport) {
      case Sport.basketball:
        return this.multiQuarterScoreFormatter.formatScore(match.score);
      case Sport.tennis:
      case Sport.volleyball:
        return this.multiSetScoreFormatter.formatScore(match.score);
      case Sport.handball:
      case Sport.soccer:
        return this.singleMatchScoreFormatter.formatScore(match.score);
      default:
        throw new UnsupportedSportException();
    }
  }

  private validateScore(score: unknown): void {
    if (score == null) {
      throw new InvalidScoreException();
    }
  }
}

Event parsing

Finish the EventParser class implementation.

export class EventParser {
  constructor(
    private readonly eventNameMaker: EventNameMaker,
    private readonly eventScoreFormatter: EventScoreFormatter
  ) {
  }

  parse(match: Partial<Match>): SportEvent {
    return {
      name: this.eventNameMaker.makeName(match),
      score: this.eventScoreFormatter.formatScore(match)
    }
  }
}

Extract input data as separate constant.

export const matches = [
  {
    sport: 'soccer',
    participant1: 'Chelsea',
    participant2: 'Arsenal',
    score: '2:1',
  },
  {
    sport: 'volleyball',
    participant1: 'Germany',
    participant2: 'France',
    score: '3:0,25:23,25:19,25:21',
  },
  {
    sport: 'handball',
    participant1: 'Pogoń Szczeciń',
    participant2: 'Azoty Puławy',
    score: '34:26',
  },
  {
    sport: 'basketball',
    participant1: 'GKS Tychy',
    participant2: 'GKS Katowice',
    score: [
      ['9:7', '2:1'],
      ['5:3', '9:9'],
    ],
  },
  {
    sport: 'tennis',
    participant1: 'Maria Sharapova',
    participant2: 'Serena Williams',
    score: '2:1,7:6,6:3,6:7',
  },
  {
    sport: 'soccer',
    // missing score
    participant1: 'Chelsea',
    participant2: 'Arsenal',
  },
  {
    // missing sport name
    score: '2:3',
    participant1: 'Chelsea',
    participant2: 'Arsenal',
  },
  {
    sport: 'tennis',
    score: '2:1,7:6,6:3,6:7',
    participant1: 'Maria Sharapova',
    // missing participant2
  },
  {
    sport: 'tennis',
    score: '2:1,7:6,6:3,6:7',
    // missing participant1
    participant2: 'Serena Williams',
  },
  {
    sport: 'hockey', // not existing sport
    participant1: 'Maria Sharapova',
    participant2: 'Serena Williams',
    score: '2:1,7:6,6:3,6:7',
  },
  {
    sport: 'tennis',
    participant1: 'Maria Sharapova',
    participant2: 'Serena Williams',
    score: '0-1', // invalid score format
  },
];

Create instances of all classes.

const matchScoreParser = new MatchScoreParser();
const singleMatchScoreFormatter = new SingleMatchScoreFormatter(matchScoreParser);
const multiQuarterScoreFormatter = new MultiQuarterScoreFormatter(singleMatchScoreFormatter);
const multiSetScoreFormatter = new MultiSetScoreFormatter(singleMatchScoreFormatter);
const eventScoreFormatter = new EventScoreFormatter(
  multiQuarterScoreFormatter,
  singleMatchScoreFormatter,
  multiSetScoreFormatter
);

const vsSeparatedNameMaker = new VsSeparatedNameMaker();
const dashSeparatedNameMaker = new DashSeparatedNameMaker();
const eventNameMaker = new EventNameMaker(vsSeparatedNameMaker, dashSeparatedNameMaker);

const eventParser = new EventParser(eventNameMaker, eventScoreFormatter);

Process the input data and prepare output data.

const parsedMatches = matches.map(match => {
  try {
    return eventParser.parse(match);
  } catch (error) {
    // ignore error
  }
}).filter(v => v != null);

console.log(parsedMatches);

Put the solution in the index.ts file and compile it to Javascript.

./node_modules/typescript/bin/tsc index.ts

Run the file.

node index.js

Validate the results.

[
  {name: 'Chelsea - Arsenal', score: '2:1'},
  {
    name: 'Germany - France',
    score: 'Main score: 3:0 (set1 25:23, set2 25:19, set3 25:21)'
  },
  {name: 'Pogoń Szczeciń vs Azoty Puławy', score: '34:26'},
  {name: 'GKS Tychy - GKS Katowice', score: '9:7,2:1,5:3,9:9'},
  {
    name: 'Maria Sharapova vs Serena Williams',
    score: 'Main score: 2:1 (set1 7:6, set2 6:3, set3 6:7)'
  }
]

Testing

The result from manual testing looks good. Nevertheless, it's not enough. To be sure that our code works, it needs to be tested.

Create a solution.spec.ts file and write the tests:

describe('when EventParser is created', () => {
  let eventParser: EventParser;

  beforeAll(() => {
    eventParser = getEventParser();
  });

  it('should throw UnsupportedSportException when "sport" propery is missing', () => {
    const input: Partial<Match> = {
      score: '2:3',
      participant1: 'Chelsea',
      participant2: 'Arsenal',
    };
    const expected = new UnsupportedSportException();
    expect(() => eventParser.parse(input)).toThrow(expected);
  });

  it('should throw UnsupportedSportException when "sport" propery is set but not recognized', () => {
    const input: Partial<Match> = {
      sport: 'hockey',
      score: '2:3',
      participant1: 'Chelsea',
      participant2: 'Arsenal',
    };
    const expected = new UnsupportedSportException();
    expect(() => eventParser.parse(input)).toThrow(expected);
  });

  it('should throw InvalidScoreException when "score" propery is missing', () => {
    const input: Partial<Match> = {
      sport: 'soccer',
      participant1: 'Chelsea',
      participant2: 'Arsenal',
    };
    const expected = new InvalidScoreException();
    expect(() => eventParser.parse(input)).toThrow(expected);
  });

  it('should throw InvalidScoreFormatException when "score" propery is missing', () => {
    const input: Partial<Match> = {
      sport: 'soccer',
      score: '3-3',
      participant1: 'Chelsea',
      participant2: 'Arsenal',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidParticipantNameException when "participant1" propery is missing', () => {
    const input: Partial<Match> = {
      sport: 'tennis',
      score: '2:1,7:6,6:3,6:7',
      participant2: 'Serena Williams',
    };
    const expected = new InvalidParticipantNameException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidParticipantNameException when "participant1" propery is an empty string', () => {
    const input: Partial<Match> = {
      sport: 'tennis',
      score: '2:1,7:6,6:3,6:7',
      participant1: '   ',
      participant2: 'Serena Williams',
    };
    const expected = new InvalidParticipantNameException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidParticipantNameException when "participant2" propery is missing', () => {
    const input: Partial<Match> = {
      sport: 'tennis',
      score: '2:1,7:6,6:3,6:7',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidParticipantNameException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidParticipantNameException when "participant2" propery is an empty string', () => {
    const input: Partial<Match> = {
      sport: 'tennis',
      score: '2:1,7:6,6:3,6:7',
      participant2: '',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidParticipantNameException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidScoreFormatException when "score" property contains invalid format', () => {
    const input: Partial<Match> = {
      sport: 'tennis',
      score: '2:1',
      participant2: 'Maria Sharapova',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidScoreFormatException when "score" property contains invalid format', () => {
    const input: Partial<Match> = {
      sport: 'basketball',
      score: [['0:1']],
      participant2: 'Maria Sharapova',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidScoreFormatException when "score" property contains invalid format', () => {
    const input: Partial<Match> = {
      sport: 'soccer',
      score: '1,2',
      participant2: 'Maria Sharapova',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidScoreFormatException when "score" property contains invalid format', () => {
    const input: Partial<Match> = {
      sport: 'handball',
      score: '1|2',
      participant2: 'Maria Sharapova',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should throw InvalidScoreFormatException when "score" property contains invalid format', () => {
    const input: Partial<Match> = {
      sport: 'volleyball',
      score: '12:2, 24:20',
      participant2: 'Maria Sharapova',
      participant1: 'Serena Williams',
    };
    const expected = new InvalidScoreFormatException();
    expect(() => eventParser.parse(input)).toThrowError(expected);
  });

  it('should return valid sport event when valid data is passed', () => {
    const inputMatches: Partial<Match>[] = [
      {
        sport: 'soccer',
        participant1: 'Chelsea',
        participant2: 'Arsenal',
        score: '2:1',
      },
      {
        sport: 'volleyball',
        participant1: 'Germany',
        participant2: 'France',
        score: '3:0,25:23,25:19,25:21',
      },
      {
        sport: 'handball',
        participant1: 'Pogoń Szczeciń',
        participant2: 'Azoty Puławy',
        score: '34:26',
      },
      {
        sport: 'basketball',
        participant1: 'GKS Tychy',
        participant2: 'GKS Katowice',
        score: [
          ['9:7', '2:1'],
          ['5:3', '9:9'],
        ],
      },
      {
        sport: 'tennis',
        participant1: 'Maria Sharapova',
        participant2: 'Serena Williams',
        score: '2:1,7:6,6:3,6:7',
      },
    ];

    const expected: SportEvent[] = [
      {name: 'Chelsea - Arsenal', score: '2:1'},
      {
        name: 'Germany - France',
        score: 'Main score: 3:0 (set1 25:23, set2 25:19, set3 25:21)'
      },
      {name: 'Pogoń Szczeciń vs Azoty Puławy', score: '34:26'},
      {name: 'GKS Tychy - GKS Katowice', score: '9:7,2:1,5:3,9:9'},
      {
        name: 'Maria Sharapova vs Serena Williams',
        score: 'Main score: 2:1 (set1 7:6, set2 6:3, set3 6:7)'
      }
    ];
    const parsedMatches = inputMatches.map(m => eventParser.parse(m));
    expect(parsedMatches).toEqual(expected);
  });

});

const getEventParser = (): EventParser => {
  const matchScoreParser = new MatchScoreParser();
  const singleMatchScoreFormatter = new SingleMatchScoreFormatter(matchScoreParser);
  const multiQuarterScoreFormatter = new MultiQuarterScoreFormatter(singleMatchScoreFormatter);
  const multiSetScoreFormatter = new MultiSetScoreFormatter(singleMatchScoreFormatter);
  const eventScoreFormatter = new EventScoreFormatter(
    multiQuarterScoreFormatter,
    singleMatchScoreFormatter,
    multiSetScoreFormatter
  );

  const vsSeparatedNameMaker = new VsSeparatedNameMaker();
  const dashSeparatedNameMaker = new DashSeparatedNameMaker();
  const eventNameMaker = new EventNameMaker(vsSeparatedNameMaker, dashSeparatedNameMaker);

  return new EventParser(eventNameMaker, eventScoreFormatter);
}

Run the tests:

jest -i solution.spec.ts

Validate the result. All are green which means that all the tests passed.

 PASS  ./solution.spec.ts
  when EventParser is created
    ✓ should throw UnsupportedSportException when "sport" propery is missing (8 ms)
    ✓ should throw UnsupportedSportException when "sport" propery is set but not recognized (1 ms)
    ✓ should throw InvalidScoreException when "score" propery is missing
    ✓ should throw InvalidScoreFormatException when "score" propery is missing
    ✓ should throw InvalidParticipantNameException when "participant1" propery is missing
    ✓ should throw InvalidParticipantNameException when "participant1" propery is an empty string (1 ms)
    ✓ should throw InvalidParticipantNameException when "participant2" propery is missing
    ✓ should throw InvalidParticipantNameException when "participant2" propery is an empty string
    ✓ should throw InvalidScoreFormatException when "score" property contains invalid format
    ✓ should throw InvalidScoreFormatException when "score" property contains invalid format
    ✓ should throw InvalidScoreFormatException when "score" property contains invalid format (1 ms)
    ✓ should throw InvalidScoreFormatException when "score" property contains invalid format
    ✓ should throw InvalidScoreFormatException when "score" property contains invalid format
    ✓ should return valid sport event when valid data is passed (1 ms)

Test Suites: 1 passed, 1 total
Tests:       14 passed, 14 total
Snapshots:   0 total
Time:        1.487 s
Ran all test suites matching /solution.spec.ts/i.

Source code

gitlab.com/barcioch-blog-examples/20230309-..