# 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:

```javascript
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.

```bash
npm init -y
```

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

```bash
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**.

```bash
npx ts-jest config:init
```

## Implementation

### Basic data structures

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

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

Create a **Match** interface responsible for the input data.

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

Create a **SportEvent** interface responsible for the output data.

```typescript
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.

```typescript
export abstract class EventParserException extends Error {

}
```

### Input data processing

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

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

  }
}
```

The partial match class (**Partial&lt; Match &gt;**) 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.

```typescript
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.

```typescript
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**.

```typescript
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.

```typescript
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.

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

```typescript
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.

```typescript
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.

```typescript
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.

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

### Parsing the match results

Create **EventScoreFormatter** class skeleton.

```typescript
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.

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

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

```typescript
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.

```typescript
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.:

```bash
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.

```typescript



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]),
    }
  }
}
```

```typescript
export class InvalidScoreFormatException extends EventParserException {
  message = 'Invalid score format';
}
```

```typescript
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.

```typescript
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.

```typescript
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
    

```typescript
this.validateScore(score);
```

* extract individual match scores into an array
    

```typescript
const matchScores = this.extractMatchScores(score as string);
```

* separate the main result from the rest
    

```typescript
const mainScore = matchScores.shift();
```

* format the results
    

```typescript
const formattedMatchScores = matchScores.map((matchScore, index) => {
  return `set${ index + 1 } ${ matchScore }`;
}).join(', ');
```

* return a fully formatted result
    

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

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

```typescript
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
    

```typescript
    this.validateScore(score);
```

* extract scores into an array with formatted scores
    

```typescript
    const matchScores = this.extractMatchScores(score as string[][]);
```

* return a fully formatted result
    

```typescript
    return matchScores.join(',');
```

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

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

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

Run the file.

```shell
node index.js
```

Validate the results.

```typescript
[
  {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:

```typescript
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:

```shell
jest -i solution.spec.ts
```

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

```bash
 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

https://gitlab.com/barcioch-blog-examples/20230309-refactoring
