Skip to content

Commit

Permalink
feat: command service
Browse files Browse the repository at this point in the history
    - process business logic: command evalutaion
    - unit test command evaluation
    - setup repo interface
  • Loading branch information
hhow09 committed Jan 21, 2025
1 parent 63a3eb6 commit f192ddb
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 0 deletions.
107 changes: 107 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@types/express": "^5.0.0",
"@types/node": "^22.10.7",
"express": "^4.21.2",
"mathjs": "^14.0.1",
"pino": "^9.6.0",
"socket.io": "^4.8.1"
},
Expand Down
44 changes: 44 additions & 0 deletions backend/src/command-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import CommandService from './command-service';
import { pino } from 'pino';

const mockRepository = {
saveCommand: jest.fn(),
getLatest: jest.fn(),
};

describe('evaluate', () => {
describe.each([
{ command: '', hasError: true },
{ command: ' ', hasError: true },
{ command: '1', expected: '1' },
{ command: '1+', hasError: true },
{ command: '2 + + 1', hasError: true },

{ command: '1 + 1', expected: '2' },
{ command: '123 * 23 + 45 - 6', expected: '2868' },
{ command: '10 * 5 / 2', expected: '25' },
{ command: '10.5 * 5 / 2.5', expected: '21' },

{ command: '1a2b3c', hasError: true },
{ command: '1 + 2 = 3', hasError: true },
{ command: '(123 * 23) + 45 - 6', hasError: true },

{ command: '1 + 1', expected: '2' },
{ command: '123 * 23 + 45 - 6', expected: '2868' },
{ command: '10 * 5 / 2', expected: '25' },
{ command: '10.54 * 7 / 3', expected: '24.593333333333334' },
{ command: '5 * 7 / 3 * 3', expected: '35' },

])('.evaluate($command)', ({ command, hasError, expected }) => {
test(`returns ${hasError}`, () => {
const clientId = 'whatever';
const commandService = new CommandService(pino(), mockRepository);
if (hasError) {
expect(() => commandService.evaluateAndSave(clientId, command)).toThrow('Invalid command');
} else {
expect(commandService.evaluateAndSave(clientId, command)).toBe(expected);
expect(mockRepository.saveCommand).toHaveBeenCalledWith(clientId, command, expected);
}
});
});
});
67 changes: 67 additions & 0 deletions backend/src/command-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Logger } from "pino";
import { evaluate as evaluateMathjs } from 'mathjs';
import { CommandAndResult } from "./entities/command-result.entity";

interface IRepository {
saveCommand(clientId: string, command: string, result: string): void;
getLatest(clientId: string, count: number): CommandAndResult[];
}

const historyCount = 10;

class CommandService {
private operators = new Set(['+', '-', '*', '/']);
private repository: IRepository;
private logger: Logger;
constructor(logger: Logger, repository: IRepository) {
this.logger = logger;
this.repository = repository;
}

// evaluateAndSave evaluates a mathematical expression and saves the result to the repository
public evaluateAndSave(clientId: string, expression: string): string {
const result = this.evaluate(expression);
this.repository.saveCommand(clientId, expression, result);
return result;
}

// getHistory returns the latest 10 commands for a client
public getHistory(clientId: string): CommandAndResult[] {
return this.repository.getLatest(clientId, historyCount);
}

// evaluate evaluate a mathematical expression
private evaluate(expression: string): string {
if (!this.isValidCommand(expression)) {
throw new Error('Invalid command');
}
return evaluateMathjs(expression).toString();
}

// isValidCommand checks if the command is valid
private isValidCommand(s: string): boolean {
s = s.replace(/ /g,''); // remove all spaces
if (s.length === 0) {
return false;
}
// allowed characters: 0-9, +, -, *, /, .
const regex = /^[\d+\-*/.]+$/;
if (!regex.test(s)) {
return false;
}
// operators cannot be adjacent to each other
for (let i = 0; i < s.length - 1; i++) {
if (this.operators.has(s[i]) && this.operators.has(s[i + 1])) {
return false;
}
}

// operators cannot be at the beginning or end of the string
if (this.operators.has(s[0]) || this.operators.has(s[s.length - 1])) {
return false;
}
return true;
}
}

export default CommandService;
5 changes: 5 additions & 0 deletions backend/src/entities/command-result.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// CommandAndResult is a round trip of a command and its result
export type CommandAndResult = {
expression: string;
result: string;
}

0 comments on commit f192ddb

Please sign in to comment.