Skip to content

Instantly share code, notes, and snippets.

@qnkhuat
Last active September 2, 2021 02:42
Show Gist options
  • Save qnkhuat/f43ea8af79c73b06261b1361e5fd99b7 to your computer and use it in GitHub Desktop.
Save qnkhuat/f43ea8af79c73b06261b1361e5fd99b7 to your computer and use it in GitHub Desktop.
`node server.js` to run. `node server.js -test` to run test. Default port is 3000
/*
# PigeonLab Engineering Test
## Overview
Your task is to create a TCP based calculator service and deploy it. It'll take
in simple math operations such as `1+1`, or `5*10` and reply with the answer.
You should implement the server in node.js, with no dependancies, and in a single
js file. Include any automated tests in the same file.
## Input / Output
The server should accept input over TCP on a port of your choosing. Input is text
based with each command followed by a newline character. Responses are also given
as text followed by a newline.
For a given connection, the output should have one line of output per line of
input. (Don't send human friendly messages on the socket.)
Note that TCP connections are streams of bytes, and one 'data' event does not
correspond to one line of input. **Your input lines might be split or merged
arbitrarily.** Note that many examples of TCP servers that you might find on google
do not handle this correctly. It's particularly important to get this right because
our testing script relies on it.
## Semantics
Each command should have the syntax `<number><operator><number>`.
Where:
* `number` is a decimal representation of an unsigned 32 bit integer.
* `operator` is one of `+`, `-`, `*`, `/`, `%`.
When an invalid command is recieved, the response should be an error. Commands
with extra whitespace are invalid.
Math operations should be evaluated according to the semantics of **unsigned 32 bit
numbers**. For example, the result of `4294967295+1` is `0`. Rounding for
division is towards zero. Division or modulo by zero should generate an error.
Errors should be represented like `error: <human readable message>`. Errors take
up one line, just like normal responses. After an error, the server should process
the next line as normal.
### Example Input
```
1+1
10*5
101/10
99%10
0-1
0/0
0.1+0.2
1 + 1
hello world
```
### Example Output
```
2
50
10
9
4294967295
error: division by zero
error: incorrect syntax
error: incorrect syntax
error: incorrect syntax
```
## Submission
Create a secret gist on https://gist.github.com and email us the link.
Please don't make your code public.
*/
let net = require('net');
// *** Error code *** //
const newError = (msg) => new Error(`error: ${msg}`);
const ErrorIncorrectSyntax = newError('incorrect syntax');
const ErrorDivisionByZero = newError('division by zero');
const ErrorModuloByZero = newError('modulo by zero');
// *** Constants ***//
const PORT = 3000;
const HOST = 'localhost';
const EOL = '\n';
const commandRegex = /^([0-9]+)([\/\+\-\*%])([0-9]+)$/;
const opFunc = {
'+': (x, y) => x + y,
'-': (x, y) => x - y,
'*': (x, y) => x * y,
'/': (x, y) => {
if (y == 0) throw ErrorDivisionByZero;
return x / y
},
'%': (x, y) => {
if (y == 0) throw ErrorModuloByZero;
return x / y
},
};
// *** Helpers *** //
const toUint32 = (x) => x >>> 0;
const calc = (x, op, y) => {
if (!(op in opFunc)) return ErrorIncorrectSyntax.message;
let result = opFunc[op](toUint32(x), toUint32(y));
return toUint32(result);
}
const handleCommand = (command) => {
let matches = command.match(commandRegex);
if (matches != null && matches.length == 4) {
try{
result = calc(matches[1], matches[2], matches[3]);
return result.toString();
} catch(e) {
return e.message;
}
} else {
return ErrorIncorrectSyntax.message
}
}
function onConnect(socket){
var line = "";
socket.on('readable', function(){
let reqBuffer = Buffer.from('');
// temporary buffer to read in chunks
let buf;
// read until reach end of data
while(true) {
buf = socket.read();
if (buf === null) break;
reqBuffer = Buffer.concat([reqBuffer, buf]);
}
let eolindex = reqBuffer.indexOf(EOL);
// not found skip till next request
if (eolindex == -1){
line += reqBuffer.toString();
return
}
// in case a new line is in the middle of reqBuffer
if (eolindex != -1 && eolindex != Buffer.byteLength(reqBuffer) - 1) {
// push back the remaining bytes to readable stream
let remaining = reqBuffer.slice(eolindex + 1, Buffer.byteLength(reqBuffer));
socket.unshift(remaining);
reqBuffer = reqBuffer.slice(0, eolindex);
}
line += reqBuffer.toString();
let command = line.trim();
socket.write(handleCommand(command));
socket.write('\n');
line = ''; // reset line
});
//Handle client connection termination.
socket.on('close',function(){
console.log(`Terminated the connection`);
});
//Handle Client connection error.
socket.on('error',function(error){
console.error(`Connection Error ${error}`);
});
};
const test = () => {
const assertEqual = (a, b, msg) => {
if (a == b){
return true
} else {
console.error(`Expected: ${a}, Got ${b}. ${msg}`);
return false
}
}
// func is function to test
// each test cases is an array which the first elem is a list of params, the second is the expected outcome
const runner = (func, cases) => {
let successes = 0;
console.log("Testing: ", func.name);
cases.forEach((xs) => {
let output = func(...xs[0]);
successes += assertEqual(output, xs[1], `Args: ${xs[0]}`);
})
console.log(`[${successes == cases.length ? "PASS" : "Fail"}] - ${successes}/${cases.length}`);
console.log("----------------------")
}
runner(toUint32,
[
[[0], 0],
[[-1], 4294967295],
[[4294967296], 0],
]
);
runner(calc,
[
[[1, "+", 1], 2],
[[10, "*", 5], 50],
[[101, "/", 10], 10],
[[99, "%", 10], 9],
[[0, "-", 1], 4294967295],
]
);
runner(handleCommand,
[
[["1+1"], "2"],
[["10*5"], "50"],
[["101/10"], "10"],
[["99%10"], "9"],
[["0-1"], "4294967295"],
[["0:1"], ErrorIncorrectSyntax.message],
[["0-1\n"], ErrorIncorrectSyntax.message],
[["0/0"], ErrorDivisionByZero.message],
[["3%0"], ErrorModuloByZero.message],
[["1 + 1"], ErrorIncorrectSyntax.message],
[["Hello world"], ErrorIncorrectSyntax.message],
]
)
}
var myArgs = process.argv;
if (myArgs.length > 1 && myArgs[2] == '-test') {
test();
} else {
let server = net.createServer(onConnect)
console.log(`Listening to: ${HOST}:${PORT}`)
server.listen(PORT, HOST);
}
@qnkhuat
Copy link
Author

qnkhuat commented Aug 10, 2021

node server.js to run. node server.js -test to run test.

Default port is 3000

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment