Compare commits

...

7 Commits

Author SHA1 Message Date
897f802659 add kata 2025-02-15 00:52:14 +01:00
5f7c1ca61a finish math parser 2025-02-05 23:04:53 +01:00
69edf86b63 wip: parser 2025-02-05 16:59:40 +01:00
857a252fea WIP: math parser 2025-02-03 18:22:26 +01:00
b3ac5ee075 WIP: math parser 2025-02-03 17:13:32 +01:00
3ffb7f8181 add kata 2025-02-03 14:28:54 +01:00
03bdd63342 move to 4kyu 2025-02-03 13:52:29 +01:00
12 changed files with 1713 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
In this Kata we are going to mimic the SQL syntax with JavaScript (or TypeScript).
To do this, you must implement the query() function. This function returns and object with the next methods:
{
select: ...,
from: ...,
where: ...,
orderBy: ...,
groupBy: ...,
having: ...,
execute: ...
}
The methods are chainable and the query is executed by calling the execute() method.
⚠️ Note: The order of appearance of a clause in a query doesn't matter. However, when it comes time for you to run the query, you MUST execute the clauses in this logical order: from first, then where, then groupBy, then having, then select and finally orderBy.
SELECT * FROM numbers
var numbers = [1, 2, 3];
query().select().from(numbers).execute(); //[1, 2, 3]
//clauses order does not matter
query().from(numbers).select().execute(); //[1, 2, 3]
Of course, you can make queries over object collections:
var persons = [
{name: 'Peter', profession: 'teacher', age: 20, maritalStatus: 'married'},
{name: 'Michael', profession: 'teacher', age: 50, maritalStatus: 'single'},
{name: 'Peter', profession: 'teacher', age: 20, maritalStatus: 'married'},
{name: 'Anna', profession: 'scientific', age: 20, maritalStatus: 'married'},
{name: 'Rose', profession: 'scientific', age: 50, maritalStatus: 'married'},
{name: 'Anna', profession: 'scientific', age: 20, maritalStatus: 'single'},
{name: 'Anna', profession: 'politician', age: 50, maritalStatus: 'married'}
];
//SELECT * FROM persons
query().select().from(persons).execute(); // [{name: 'Peter',...}, {name: 'Michael', ...}]
You can select some fields:
function profession(person) {
return person.profession;
}
//SELECT profession FROM persons
query().select(profession).from(persons).execute(); //select receives a function that will be called with the values of the array //["teacher","teacher","teacher","scientific","scientific","scientific","politician"]
If you repeat a SQL clause (except where() or having()), an exception will be thrown
query().select().select().execute(); //Error('Duplicate SELECT');
query().select().from([]).select().execute(); //Error('Duplicate SELECT');
query().select().from([]).from([]).execute(); //Error('Duplicate FROM');
query().select().from([]).where().where() //This is an AND filter (see below)
You can omit any SQLclause:
var numbers = [1, 2, 3];
query().select().execute(); //[]
query().from(numbers).execute(); // [1, 2, 3]
query().execute(); // []
You can apply filters:
function isTeacher(person) {
return person.profession === 'teacher';
}
//SELECT profession FROM persons WHERE profession="teacher"
query().select(profession).from(persons).where(isTeacher).execute(); //["teacher", "teacher", "teacher"]
//SELECT * FROM persons WHERE profession="teacher"
query().select().from(persons).where(isTeacher).execute(); //[{person: 'Peter', profession: 'teacher', ...}, ...]
function name(person) {
return person.name;
}
//SELECT name FROM persons WHERE profession="teacher"
query().select(name).from(persons).where(isTeacher).execute();//["Peter", "Michael", "Peter"]
Agrupations are also possible:
//SELECT * FROM persons GROUP BY profession <- Bad in SQL but possible in this kata
query().select().from(persons).groupBy(profession).execute();
[
["teacher",
[
{
name: "Peter",
profession: "teacher"
...
},
{
name: "Michael",
profession: "teacher"
...
}
]
],
["scientific",
[
{
name: "Anna",
profession: "scientific"
},
...
]
]
...
]
You can mix where() with groupBy():
//SELECT * FROM persons WHERE profession='teacher' GROUP BY profession
query().select().from(persons).where(isTeacher).groupBy(profession).execute();
Or with select():
function professionGroup(group) {
return group[0];
}
//SELECT profession FROM persons GROUP BY profession
query().select(professionGroup).from(persons).groupBy(profession).execute(); //["teacher","scientific","politician"]
Another example:
function isEven(number) {
return number % 2 === 0;
}
function parity(number) {
return isEven(number) ? 'even' : 'odd';
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
//SELECT * FROM numbers
query().select().from(numbers).execute(); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
//SELECT * FROM numbers GROUP BY parity
query().select().from(numbers).groupBy(parity).execute(); //[["odd",[1,3,5,7,9]],["even",[2,4,6,8]]]
Multilevel grouping:
function isPrime(number) {
if (number < 2) {
return false;
}
var divisor = 2;
for(; number % divisor !== 0; divisor++);
return divisor === number;
}
function prime(number) {
return isPrime(number) ? 'prime' : 'divisible';
}
//SELECT * FROM numbers GROUP BY parity, isPrime
query().select().from(numbers).groupBy(parity, prime).execute(); // [["odd",[["divisible",[1,9]],["prime",[3,5,7]]]],["even",[["prime",[2]],["divisible",[4,6,8]]]]]
orderBy should be called after groupBy, so the values passed to orderBy function are the grouped results by the groupBy function.
Filter groups with having():
function odd(group) {
return group[0] === 'odd';
}
//SELECT * FROM numbers GROUP BY parity HAVING odd(number) = true <- I know, this is not a valid SQL statement, but you can understand what I am doing
query().select().from(numbers).groupBy(parity).having(odd).execute(); //[["odd",[1,3,5,7,9]]]
You can order the results:
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
function descendentCompare(number1, number2) {
return number2 - number1;
}
//SELECT * FROM numbers ORDER BY value DESC
query().select().from(numbers).orderBy(descendentCompare).execute(); //[9,8,7,6,5,4,3,2,1]
from() supports multiple collections:
var teachers = [
{
teacherId: '1',
teacherName: 'Peter'
},
{
teacherId: '2',
teacherName: 'Anna'
}
];
var students = [
{
studentName: 'Michael',
tutor: '1'
},
{
studentName: 'Rose',
tutor: '2'
}
];
function teacherJoin(join) {
return join[0].teacherId === join[1].tutor;
}
function student(join) {
return {studentName: join[1].studentName, teacherName: join[0].teacherName};
}
//SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor
query().select(student).from(teachers, students).where(teacherJoin).execute(); //[{"studentName":"Michael","teacherName":"Peter"},{"studentName":"Rose","teacherName":"Anna"}]
Finally, where() and having() admit multiple AND and OR filters:
function tutor1(join) {
return join[1].tutor === "1";
}
//SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor AND tutor = 1
query().select(student).from(teachers, students).where(teacherJoin).where(tutor1).execute(); //[{"studentName":"Michael","teacherName":"Peter"}] <- AND filter
var numbers = [1, 2, 3, 4, 5, 7];
function lessThan3(number) {
return number < 3;
}
function greaterThan4(number) {
return number > 4;
}
//SELECT * FROM number WHERE number < 3 OR number > 4
query().select().from(numbers).where(lessThan3, greaterThan4).execute(); //[1, 2, 5, 7] <- OR filter
var numbers = [1, 2, 1, 3, 5, 6, 1, 2, 5, 6];
function greatThan1(group) {
return group[1].length > 1;
}
function isPair(group) {
return group[0] % 2 === 0;
}
function id(value) {
return value;
}
function frequency(group) {
return { value: group[0], frequency: group[1].length };
}
//SELECT number, count(number) FROM numbers GROUP BY number HAVING count(number) > 1 AND isPair(number)
query().select(frequency).from(numbers).groupBy(id).having(greatThan1).having(isPair).execute(); // [{"value":2,"frequency":2},{"value":6,"frequency":2}])
Requirements Recap
Clause ⚠️ Must be executed... Arg(s) Count Arg Type Repeatable?
from First 1 or More (=> cartesian product of specified tables) Table(s) (i.e., arrays) No
where Second 1 or More (=> to be logically OR'd) Functions Yes (each repetition is a logical AND)
groupBy Third 1 or More (=> groups by the 1st fn, then, within each subgroup, groups by the 2nd fn, ...) Functions No
having Fourth 1 or More (=> to be logically OR'd) Functions Yes (each repetition is a logical AND)
select Fifth 0 (selects everything) or 1 Function No
orderBy Last 1 Function No
execute - None (just executes the entire query) - -
If any of the unrepeatable clauses are repeated in the query, your solution MUST raise an Error object with the error message "duplicate " followed by the name of the duplicated clause. If the clause is multi-word, merge it into one (ex: groupby).
For example, if the groupBy clause is duplicated, you should throw an Error with the exact string message "duplicate groupby" (capitalization doesn't matter).

View File

@@ -0,0 +1,232 @@
type GroupedData = Map<string, any[] | GroupedData>;
class Query {
private data: any[];
private selector: (obj: any) => any = (obj) => obj;
private filterGroups: ((obj: any) => boolean)[][] = [];
private groupBySelectors: ((obj: any) => any)[] = [];
private groupFilterGroups: ((obj: any) => boolean)[][] = [];
private orderFunction: (obj1: any, obj2: any) => number = (obj1, obj2) => 0;
constructor() {
this.data = [];
}
// If you repeat a SQL clause (except where() or having()), an exception will be thrown
private _selected = false;
private _fromed = false;
private _grouped = false;
private _ordered = false;
select(newSelector?: (obj: any) => any) {
if (this._selected) {
throw new Error('Duplicate SELECT');
}
if (newSelector) {
this.selector = newSelector;
};
this._selected = true;
return this;
}
from(...dataRows: any[][]) {
if (this._fromed) {
throw new Error('Duplicate FROM');
}
this._fromed = true;
// cartesian product of all data arrays
dataRows.forEach((dataRow) => {
if (this.data.length === 0) {
this.data = dataRow;
return;
}
const newData: any[] = [];
this.data.forEach((obj) => {
dataRow.forEach((newObj) => {
newData.push([obj, newObj]);
});
})
this.data = newData;
})
return this;
}
where(...newFilters: ((obj: any) => boolean)[]) {
this.filterGroups.push(newFilters);
return this;
}
groupBy(...newGroupBySelectors: ((obj: any) => any)[]) {
if (this._grouped) {
throw new Error('Duplicate GROUPBY');
}
this._grouped = true;
this.groupBySelectors = newGroupBySelectors;
return this;
}
having(...newGroupFilters: ((obj: any) => boolean)[]) {
this.groupFilterGroups.push(newGroupFilters);
return this;
}
orderBy(newOrderFunction: (obj1: any, obj2: any) => number) {
if (this._ordered) {
throw new Error('Duplicate ORDERBY');
}
this._ordered = true;
this.orderFunction = newOrderFunction;
return this;
}
execute() {
// filter
this.data = this.executeFilter();
// group by
const groupedMap = this.executeGroupBy();
// filter groups
this.executeHaving(groupedMap);
const selected = this.executeSelect(groupedMap);
const result = this.executeOrderBy(selected);
return result;
}
private executeFilter() {
// this.filterGroups is a list of groups of filters
// filters in the same group are ORed
// groups are ANDed
let filteredData = this.data;
this.filterGroups.forEach((filterGroup) => {
filteredData = filteredData.filter((obj) => {
return filterGroup.some((filter) => filter(obj));
});
})
return filteredData;
}
private groupOnce(data: any[], groupBySelector: (obj: any) => any) {
const groupedData = new Map<string, any[] | GroupedData>();
data.forEach((obj) => {
const key = groupBySelector(obj);
if (!groupedData.has(key)) {
groupedData.set(key, []);
}
(groupedData.get(key) as any[]).push(obj);
});
return groupedData;
}
private groupRecursively(data: any[], groupBySelectors: ((obj: any) => any)[]) {
const [groupBySelector, ...restGroupBySelectors] = groupBySelectors;
const groupedData = this.groupOnce(data, groupBySelector);
if (restGroupBySelectors.length === 0) {
return groupedData;
}
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as any[];
groupedData.set(key, this.groupRecursively(group, restGroupBySelectors) as Map<string, any[]>);
});
return groupedData;
}
private executeGroupBy(filteredData: any[] = this.data) {
if (this.groupBySelectors.length === 0) {
return new Map<string, any[]>([['', filteredData]]);
}
const groupedData = this.groupRecursively(filteredData, this.groupBySelectors);
return groupedData;
}
private executeHaving(groupedData: GroupedData) {
if (this.groupFilterGroups.length === 0) return groupedData;
// filter groups
// groupFilterGroups is a list of groups of filters
// filters in the same group are ORed
// groups are ANDed
// first flatten the map to arrays
let flattened: { flatGroup: any[], key: string }[] = [];
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as GroupedData | any[];
const flattenedGroup = Array.isArray(group) ?
group
: this.flattenMapToArrays(group as GroupedData, this.selector);
flattened.push({ flatGroup: [key, flattenedGroup], key });
});
this.groupFilterGroups.forEach((groupFilters) => {
flattened = flattened.filter((group) => {
const toKeep = groupFilters.some((filter) => filter(group.flatGroup));
if (!toKeep) groupedData.delete(group.key);
return toKeep;
})
});
return groupedData;
}
private flattenMapToArrays(groupedData: GroupedData, selector?: (obj: any) => any) {
// flatten maps to arrays
const result: any[] = [];
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as GroupedData | any[];
const flattened = Array.isArray(group) ?
selector ? (group as any[]).map(selector)
: group as any[]
: this.flattenMapToArrays(group as GroupedData, selector)
result.push([key, flattened]);
});
return result;
}
private executeSelect(groupedData: GroupedData) {
const keys = Array.from(groupedData.keys());
if (keys.length === 1 && keys[0] === '') {
return (groupedData.get('') as any[])?.map(this.selector);
}
return Array.from(keys).map((key) => {
const group = groupedData.get(key) as GroupedData | any[];
if (this.groupBySelectors.length === 0) {
// no group by was done,
// selector selects from objects
return [key, (group as any[]).map(this.selector)];
}
const flattenedGroup = Array.isArray(group) ?
group
: this.flattenMapToArrays(group as GroupedData, this.selector);
// selector selects from groups
return this.selector([key, flattenedGroup]);
});
}
private executeOrderBy(selected: any[]) {
return selected.sort(this.orderFunction);
}
}
export function query() {
const q = new Query();
return q;
};

View File

@@ -0,0 +1,632 @@
import {
query
} from './solution';
import {
expect
} from "chai";
describe("SQL tests", function() {
it("Basic SELECT tests", () => {
var numbers = [1, 2, 3];
expect(query().select().from(numbers).execute()).to.deep.equal(numbers);
expect(query().select().execute()).to.deep.equal([], 'No FROM clause produces empty array');
expect(query().from(numbers).execute()).to.deep.equal(numbers, 'SELECT can be omited');
expect(query().execute()).to.deep.equal([]);
expect(query().from(numbers).select().execute()).to.deep.equal(numbers, 'The order does not matter');
});
it("Basic SELECT and WHERE over objects", () => {
var persons = [{
name: 'Peter',
profession: 'teacher',
age: 20,
maritalStatus: 'married'
},
{
name: 'Michael',
profession: 'teacher',
age: 50,
maritalStatus: 'single'
},
{
name: 'Peter',
profession: 'teacher',
age: 20,
maritalStatus: 'married'
},
{
name: 'Anna',
profession: 'scientific',
age: 20,
maritalStatus: 'married'
},
{
name: 'Rose',
profession: 'scientific',
age: 50,
maritalStatus: 'married'
},
{
name: 'Anna',
profession: 'scientific',
age: 20,
maritalStatus: 'single'
},
{
name: 'Anna',
profession: 'politician',
age: 50,
maritalStatus: 'married'
}
];
expect(query().select().from(persons).execute()).to.deep.equal(persons);
function profession(person: any) {
return person.profession;
}
// SELECT profession FROM persons
expect(query().select(profession).from(persons).execute()).to.deep.equal(["teacher", "teacher", "teacher", "scientific", "scientific", "scientific", "politician"]);
expect(query().select(profession).execute()).to.deep.equal([], 'No FROM clause produces empty array');
function isTeacher(person: any) {
return person.profession === 'teacher';
}
// SELECT profession FROM persons WHERE profession="teacher"
expect(query().select(profession).from(persons).where(isTeacher).execute()).to.deep.equal(["teacher", "teacher", "teacher"]);
// SELECT * FROM persons WHERE profession="teacher"
expect(query().from(persons).where(isTeacher).execute()).to.deep.equal(persons.slice(0, 3));
function name(person: any) {
return person.name;
}
// SELECT name FROM persons WHERE profession="teacher"
expect(query().select(name).from(persons).where(isTeacher).execute()).to.deep.equal(["Peter", "Michael", "Peter"]);
expect(query().where(isTeacher).from(persons).select(name).execute()).to.deep.equal(["Peter", "Michael", "Peter"]);
});
it("GROUP BY tests", () => {
var persons = [{
name: 'Peter',
profession: 'teacher',
age: 20,
maritalStatus: 'married'
},
{
name: 'Michael',
profession: 'teacher',
age: 50,
maritalStatus: 'single'
},
{
name: 'Peter',
profession: 'teacher',
age: 20,
maritalStatus: 'married'
},
{
name: 'Anna',
profession: 'scientific',
age: 20,
maritalStatus: 'married'
},
{
name: 'Rose',
profession: 'scientific',
age: 50,
maritalStatus: 'married'
},
{
name: 'Anna',
profession: 'scientific',
age: 20,
maritalStatus: 'single'
},
{
name: 'Anna',
profession: 'politician',
age: 50,
maritalStatus: 'married'
}
];
function profession(person: any) {
return person.profession;
}
// SELECT * FROM persons GROUPBY profession <- Bad in SQL but possible in JavaScript
expect(query().select().from(persons).groupBy(profession).execute()).to.deep.equal([
["teacher", [{
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Michael",
"profession": "teacher",
"age": 50,
"maritalStatus": "single"
}, {
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}]],
["scientific", [{
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Rose",
"profession": "scientific",
"age": 50,
"maritalStatus": "married"
}, {
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "single"
}]],
["politician", [{
"name": "Anna",
"profession": "politician",
"age": 50,
"maritalStatus": "married"
}]]
]);
function isTeacher(person: any) {
return person.profession === 'teacher';
}
// SELECT * FROM persons WHERE profession='teacher' GROUPBY profession
expect(query().select().from(persons).where(isTeacher).groupBy(profession).execute()).to.deep.equal([
["teacher", [{
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Michael",
"profession": "teacher",
"age": 50,
"maritalStatus": "single"
}, {
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}]]
]);
function professionGroup(group: any) {
return group[0];
}
// SELECT profession FROM persons GROUPBY profession
expect(query().select(professionGroup).from(persons).groupBy(profession).execute()).to.deep.equal(["teacher", "scientific", "politician"]);
function name(person: any) {
return person.name;
}
// SELECT * FROM persons WHERE profession='teacher' GROUPBY profession, name
expect(query().select().from(persons).groupBy(profession, name).execute()).to.deep.equal([
["teacher", [
["Peter", [{
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}]],
["Michael", [{
"name": "Michael",
"profession": "teacher",
"age": 50,
"maritalStatus": "single"
}]]
]],
["scientific", [
["Anna", [{
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "single"
}]],
["Rose", [{
"name": "Rose",
"profession": "scientific",
"age": 50,
"maritalStatus": "married"
}]]
]],
["politician", [
["Anna", [{
"name": "Anna",
"profession": "politician",
"age": 50,
"maritalStatus": "married"
}]]
]]
]);
function age(person: any) {
return person.age;
}
function maritalStatus(person: any) {
return person.maritalStatus;
}
// SELECT * FROM persons WHERE profession='teacher' GROUPBY profession, name, age
expect(query().select().from(persons).groupBy(profession, name, age, maritalStatus).execute()).to.deep.equal([
["teacher", [
["Peter", [
[20, [
["married", [{
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}, {
"name": "Peter",
"profession": "teacher",
"age": 20,
"maritalStatus": "married"
}]]
]]
]],
["Michael", [
[50, [
["single", [{
"name": "Michael",
"profession": "teacher",
"age": 50,
"maritalStatus": "single"
}]]
]]
]]
]],
["scientific", [
["Anna", [
[20, [
["married", [{
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "married"
}]],
["single", [{
"name": "Anna",
"profession": "scientific",
"age": 20,
"maritalStatus": "single"
}]]
]]
]],
["Rose", [
[50, [
["married", [{
"name": "Rose",
"profession": "scientific",
"age": 50,
"maritalStatus": "married"
}]]
]]
]]
]],
["politician", [
["Anna", [
[50, [
["married", [{
"name": "Anna",
"profession": "politician",
"age": 50,
"maritalStatus": "married"
}]]
]]
]]
]]
]);
function professionCount(group: any) {
return [group[0], group[1].length];
}
// SELECT profession, count(profession) FROM persons GROUPBY profession
expect(query().select(professionCount).from(persons).groupBy(profession).execute()).to.deep.equal([
["teacher", 3],
["scientific", 3],
["politician", 1]
]);
function naturalCompare(value1: any, value2: any) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
// SELECT profession, count(profession) FROM persons GROUPBY profession ORDER BY profession
expect(query().select(professionCount).from(persons).groupBy(profession).orderBy(naturalCompare).execute()).to.deep.equal([
["politician", 1],
["scientific", 3],
["teacher", 3]
]);
});
it("Number tests", () => {
function isEven(number: number) {
return number % 2 === 0;
}
function parity(number: number) {
return isEven(number) ? 'even' : 'odd';
}
function isPrime(number: number) {
if (number < 2) {
return false;
}
var divisor = 2;
for (; number % divisor !== 0; divisor++);
return divisor === number;
}
function prime(number: number) {
return isPrime(number) ? 'prime' : 'divisible';
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// SELECT * FROM numbers
expect(query().select().from(numbers).execute()).to.deep.equal(numbers);
// SELECT * FROM numbers GROUP BY parity
expect(query().select().from(numbers).groupBy(parity).execute()).to.deep.equal([
["odd", [1, 3, 5, 7, 9]],
["even", [2, 4, 6, 8]]
]);
// SELECT * FROM numbers GROUP BY parity, isPrime
expect(query().select().from(numbers).groupBy(parity, prime).execute()).to.deep.equal([
["odd", [
["divisible", [1, 9]],
["prime", [3, 5, 7]]
]],
["even", [
["prime", [2]],
["divisible", [4, 6, 8]]
]]
]);
function odd(group: any) {
return group[0] === 'odd';
}
// SELECT * FROM numbers GROUP BY parity HAVING
expect(query().select().from(numbers).groupBy(parity).having(odd).execute()).to.deep.equal([
["odd", [1, 3, 5, 7, 9]]
]);
function descendentCompare(number1: number, number2: number) {
return number2 - number1;
}
// SELECT * FROM numbers ORDER BY value DESC
expect(query().select().from(numbers).orderBy(descendentCompare).execute()).to.deep.equal([9, 8, 7, 6, 5, 4, 3, 2, 1]);
function lessThan3(number: number) {
return number < 3;
}
function greaterThan4(number: number) {
return number > 4;
}
// SELECT * FROM number WHERE number < 3 OR number > 4
expect(query().select().from(numbers).where(lessThan3, greaterThan4).execute()).to.deep.equal([1, 2, 5, 6, 7, 8, 9]);
});
it("Frequency tests", () => {
var persons = [
['Peter', 3],
['Anna', 4],
['Peter', 7],
['Michael', 10]
];
function nameGrouping(person: any) {
return person[0];
}
function sumValues(value: any) {
return [value[0], value[1].reduce(function(result: any, person: any) {
return result + person[1];
}, 0)];
}
function naturalCompare(value1: any, value2: any) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
// SELECT name, sum(value) FROM persons ORDER BY naturalCompare GROUP BY nameGrouping
expect(query().select(sumValues).from(persons).orderBy(naturalCompare).groupBy(nameGrouping).execute()).to.deep.equal([
["Anna", 4],
["Michael", 10],
["Peter", 10]
]);
var numbers = [1, 2, 1, 3, 5, 6, 1, 2, 5, 6];
function id(value: any) {
return value;
}
function frequency(group: any) {
return {
value: group[0],
frequency: group[1].length
};
}
// SELECT number, count(number) FROM numbers GROUP BY number
expect(query().select(frequency).from(numbers).groupBy(id).execute()).to.deep.equal([{
"value": 1,
"frequency": 3
}, {
"value": 2,
"frequency": 2
}, {
"value": 3,
"frequency": 1
}, {
"value": 5,
"frequency": 2
}, {
"value": 6,
"frequency": 2
}]);
function greatThan1(group: any) {
return group[1].length > 1;
}
function isPair(group: any) {
return group[0] % 2 === 0;
}
// SELECT number, count(number) FROM numbers GROUP BY number HAVING count(number) > 1 AND isPair(number)
expect(query().select(frequency).from(numbers).groupBy(id).having(greatThan1).having(isPair).execute()).to.deep.equal([{
"value": 2,
"frequency": 2
}, {
"value": 6,
"frequency": 2
}]);
});
it("Join tests", () => {
var teachers = [{
teacherId: '1',
teacherName: 'Peter'
},
{
teacherId: '2',
teacherName: 'Anna'
}
];
var students = [{
studentName: 'Michael',
tutor: '1'
},
{
studentName: 'Rose',
tutor: '2'
}
];
function teacherJoin(join: any) {
return join[0].teacherId === join[1].tutor;
}
function student(join: any) {
return {
studentName: join[1].studentName,
teacherName: join[0].teacherName
};
}
// SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor
expect(query().select(student).from(teachers, students).where(teacherJoin).execute()).to.deep.equal([{
"studentName": "Michael",
"teacherName": "Peter"
}, {
"studentName": "Rose",
"teacherName": "Anna"
}]);
var numbers1 = [1, 2];
var numbers2 = [4, 5];
expect(query().select().from(numbers1, numbers2).execute()).to.deep.equal([
[1, 4],
[1, 5],
[2, 4],
[2, 5]
]);
function tutor1(join: any) {
return join[1].tutor === "1";
}
// SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor AND tutor = 1
expect(query().select(student).from(teachers, students).where(teacherJoin).where(tutor1).execute()).to.deep.equal([{
"studentName": "Michael",
"teacherName": "Peter"
}]);
expect(query().where(teacherJoin).select(student).where(tutor1).from(teachers, students).execute()).to.deep.equal([{
"studentName": "Michael",
"teacherName": "Peter"
}]);
});
it("Duplication exception tests", () => {
function checkError(fn: any, duplicate: any) {
try {
fn();
expect(false).to.equal(false, 'An error should be throw');
} catch (e) {
expect(e instanceof Error).to.equal(true);
expect((e as Error).message).to.equal('Duplicate ' + duplicate);
}
}
function id(value: any) {
return value;
}
checkError(function() {
query().select().select().execute();
}, 'SELECT');
checkError(function() {
query().select().from([]).select().execute();
}, 'SELECT');
checkError(function() {
query().select().from([]).from([]).execute();
}, 'FROM');
checkError(function() {
query().select().from([]).orderBy(id).orderBy(id).execute();
}, 'ORDERBY');
checkError(function() {
query().select().groupBy(id).from([]).groupBy(id).execute();
}, 'GROUPBY');
});
});

View File

@@ -0,0 +1,41 @@
Instructions
Given a mathematical expression as a string you must return the result as a number.
Numbers
Number may be both whole numbers and/or decimal numbers. The same goes for the returned result.
Operators
You need to support the following mathematical operators:
Multiplication *
Division / (as floating point division)
Addition +
Subtraction -
Operators are always evaluated from left-to-right, and * and / must be evaluated before + and -.
Parentheses
You need to support multiple levels of nested parentheses, ex. (2 / (2 + 3.33) * 4) - -6
Whitespace
There may or may not be whitespace between numbers and operators.
An addition to this rule is that the minus sign (-) used for negating numbers and parentheses will never be separated by whitespace. I.e all of the following are valid expressions.
1-1 // 0
1 -1 // 0
1- 1 // 0
1 - 1 // 0
1- -1 // 2
1 - -1 // 2
1--1 // 2
6 + -(4) // 2
6 + -( -4) // 10
And the following are invalid expressions
1 - - 1 // Invalid
1- - 1 // Invalid
6 + - (4) // Invalid
6 + -(- 4) // Invalid
Validation
You do not need to worry about validation - you will only receive valid mathematical expressions following the above rules.

View File

@@ -0,0 +1,359 @@
type TokenType = 'number' | 'operator' | 'parenthesis' | 'decimal';
type TokenValue = number | '+' | '-' | '*' | '/' | '(' | ')' | '.';
type Token = NumberToken | OperatorToken | ParenthesisToken | DecimalToken;
type OperatorChars = '+' | '-' | '*' | '/';
type OperatorToken = {
type: 'operator',
value: OperatorChars,
}
const isOperatorChar = (char: string): char is OperatorChars => {
return char === '+' || char === '-' || char === '*' || char === '/';
}
type NumberToken = {
type: 'number',
value: string,
}
type ParenthesisChars = '(' | ')';
type ParenthesisToken = {
type: 'parenthesis',
value: ParenthesisChars,
}
const isParenthesisChar = (char: string): char is ParenthesisChars => {
return char === '(' || char === ')';
}
type DecimalChars = '.';
type DecimalToken = {
type: 'decimal',
value: DecimalChars,
}
const isDecimalChar = (char: string): char is DecimalChars => {
return char === '.';
}
export const tokenize = (expression: string): Token[] => {
const tokens: Token[] = [];
const chars = expression.split('');
let done = false;
let i = 0;
while (!done) {
if (i >= chars.length) {
done = true;
continue;
}
const char = chars[i];
if (char === ' ') {
i++;
continue;
}
// parenthesis
if (isParenthesisChar(char)) {
tokens.push({ type: 'parenthesis', value: char });
i++;
continue;
}
// operator
if (isOperatorChar(char)) {
tokens.push({ type: 'operator', value: char });
i++;
continue;
}
// number
if (char.match(/\d/)) {
let number = '';
while (chars[i] && chars[i].match(/\d/)) {
number += chars[i];
i++;
}
tokens.push({ type: 'number', value: number });
continue;
}
// decimal
if (isDecimalChar(char)) {
tokens.push({ type: 'decimal', value: char });
i++;
continue;
}
throw new Error('Invalid character');
}
return tokens;
}
abstract class ASTNodeCommon {
abstract readonly type: string;
abstract get value(): number;
}
type ASTNode = NumberNode | OperatorNode | GroupNode | NegationNode;
class NumberNode extends ASTNodeCommon {
private _value: number;
type = 'numbernode';
constructor(numValue: number) {
super();
this._value = numValue;
}
get value() {
return this._value;
}
static fromToken(token: NumberToken): NumberNode {
return new NumberNode(parseFloat(token.value));
}
static fromTokens(tokens: [NumberToken, DecimalToken, NumberToken]): NumberNode {
return new NumberNode(parseFloat(tokens[0].value + '.' + tokens[2].value));
}
}
class OperatorNode extends ASTNodeCommon {
type = 'operatornode';
constructor(public operator: OperatorChars, public left: ASTNode, public right: ASTNode) {
super();
}
get value(): number {
const left = this.left.value as number;
const right = this.right.value as number;
switch (this.operator) {
case '+':
return left + right;
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
default:
throw new Error('Invalid operator');
}
}
}
class GroupNode extends ASTNodeCommon {
type = 'groupnode';
constructor(public child: ASTNode) {
super();
}
get value(): number {
return this.child.value as number;
}
static fromTokens(tokens: (Token | ASTNode)[]): GroupNode {
const node = parse(tokens);
return new GroupNode(node);
}
}
class NegationNode extends ASTNodeCommon {
type = 'negationnode';
constructor(public child: NumberNode | GroupNode) {
super();
}
get value(): number {
return -(this.child.value as number);
}
}
/**
* Transforms number tokens into NumberNodes
*/
function parseNumbers(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
if (token.type !== 'number') {
i++;
continue;
};
// check if it is a decimal
const nextToken = tokens[i + 1];
const nextNextToken = tokens[i + 2];
const nextTokenType = nextToken?.type;
const nextNextTokenType = nextNextToken?.type;
if (nextTokenType === 'decimal' && nextNextTokenType === 'number') {
// it is a decimal number
const numberNode = NumberNode.fromTokens([
token as NumberToken,
nextToken as DecimalToken,
nextNextToken as NumberToken
]);
tokens.splice(i, 3, numberNode);
i += 2;
continue;
} else {
// it is a regular number
const numberNode = NumberNode.fromToken(token as NumberToken);
tokens.splice(i, 1, numberNode);
}
i++;
}
return tokens;
}
/**
* Matches highest level parenthesis and replaces them with GroupNodes.
* Only the highest level parenthesis are required because GroupNode will
* recursively parse the rest of the expression.
*/
function parseGroups(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
// match highest level parenthesis
let i = 0;
const parenthesisPairs: [number, number][] = [];
const startParenthesis: number[] = [];
while (i < tokens.length) {
const token = tokens[i];
if (token.type !== 'parenthesis') {
i++;
continue;
};
// it is a parenthesis
if (token.value === '(') {
startParenthesis.push(i);
} else {
const start = startParenthesis.pop();
if (startParenthesis.length === 0) {
if (start === undefined) {
throw new Error('Invalid parenthesis');
}
parenthesisPairs.push([start, i]);
}
}
i++;
}
// slice out parenthesis pairs and replace with GroupNode
for (const [start, end] of parenthesisPairs) {
const groupNode = GroupNode.fromTokens(tokens.slice(start + 1, end));
tokens.splice(start, end - start + 1, groupNode);
// update pairs after splice
for (let i = 0; i < parenthesisPairs.length; i++) {
const [start2, end2] = parenthesisPairs[i];
parenthesisPairs[i] = [start2 - (end - start), end2 - (end - start)];
}
}
return tokens;
}
function parseNegation(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
if (token.type !== 'operator') {
i++;
continue;
};
if (token.value === '-') {
const prevToken = tokens[i - 1] as Token;
if (!prevToken || isOperatorChar(prevToken.value)) {
// it is a negation
const child = tokens[i + 1] as NumberNode | GroupNode;
const negationNode = new NegationNode(child);
tokens.splice(i, 2, negationNode);
}
}
i++;
}
return tokens;
}
function parseMultDiv(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
if (token.type !== 'operator') {
i++;
continue;
};
if (token.value === '*' || token.value === '/') {
const left = tokens[i - 1] as ASTNode;
const right = tokens[i + 1] as ASTNode;
const operator = token.value as OperatorChars;
const operatorNode = new OperatorNode(operator, left, right);
tokens.splice(i - 1, 3, operatorNode);
continue;
}
i++;
}
return tokens;
}
function parseAddSub(tokens: (Token | ASTNode)[]): (Token | ASTNode)[] {
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
if (token.type !== 'operator') {
i++;
continue;
};
if (token.value === '+' || token.value === '-') {
const left = tokens[i - 1] as ASTNode;
const right = tokens[i + 1] as ASTNode;
const operator = token.value as OperatorChars;
const operatorNode = new OperatorNode(operator, left, right);
tokens.splice(i - 1, 3, operatorNode);
continue;
}
i++;
}
return tokens;
}
export function parse(tokens: (Token | ASTNode)[]): ASTNode {
// array that we will save intermediate results in
const transform = [...tokens];
// first pass: parse groups (parenthesis)
parseGroups(transform);
// second pass: parse numbers and decimals
parseNumbers(transform);
// third pass: negation
parseNegation(transform);
// fourth pass: multiplication and division
parseMultDiv(transform);
// fifth pass: addition and subtraction
parseAddSub(transform);
console.log(JSON.stringify(transform[0], null, 2));
return transform[0] as ASTNode;
}
export function calc(expression: string): number {
// evaluate `expression` and return result
return parse(tokenize(expression)).value;
}

View File

@@ -0,0 +1,117 @@
import { calc, tokenize } from './solution';
import { expect } from "chai";
var tests: [string, number][] = [
['1+1', 2],
['1 - 1', 0],
['1* 1', 1],
['1 /1', 1],
['-123', -123],
['123', 123],
['2 /2+3 * 4.75- -6', 21.25],
['12* 123', 1476],
['2 / (2 + 3) * 4.33 - -6', 7.732],
['(2 + (2 + 2) / 4) * 3', 9],
['(1 - 2) + -(-(-(-4)))', 3],
['((2.33 / (2.9+3.5)*4) - -6)', 7.45625]
];
describe("calc", function () {
it("should evaluate correctly", () => {
tests.forEach(function (m) {
var x = calc(m[0]);
var y = m[1];
expect(x).to.equal(y, 'Expected: "' + m[0] + '" to be ' + y + ' but got ' + x);
});
});
});
describe("tokenize", function () {
it("should tokenize correctly", () => {
expect(tokenize('1 + 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '+'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 - 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '-'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 * 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '*'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('1 / 1')).to.deep.equal([{
type: 'number',
value: '1'
}, {
type: 'operator',
value: '/'
}, {
type: 'number',
value: '1'
}]);
expect(tokenize('-3.1415')).to.deep.equal([{
type: 'operator',
value: '-'
}, {
type: 'number',
value: '3'
}, {
type: 'decimal',
value: '.'
}, {
type: 'number',
value: '1415'
}]);
expect(tokenize('3.1415')).to.deep.equal([{
type: 'number',
value: '3'
}, {
type: 'decimal',
value: '.'
}, {
type: 'number',
value: '1415'
}]);
expect(tokenize('(3 + 5) / 3')).to.deep.equal([{
type: 'parenthesis',
value: '('
}, {
type: 'number',
value: '3'
}, {
type: 'operator',
value: '+'
}, {
type: 'number',
value: '5'
}, {
type: 'parenthesis',
value: ')'
}, {
type: 'operator',
value: '/'
}, {
type: 'number',
value: '3'
}]);
});
});

View File

@@ -0,0 +1,21 @@
Consider the following numbers (where n! is factorial(n)):
u1 = (1 / 1!) * (1!)
u2 = (1 / 2!) * (1! + 2!)
u3 = (1 / 3!) * (1! + 2! + 3!)
...
un = (1 / n!) * (1! + 2! + 3! + ... + n!)
Which will win: 1 / n! or (1! + 2! + 3! + ... + n!)?
Are these numbers going to 0 because of 1/n! or to infinity due to the sum of factorials or to another number?
Task
Calculate (1 / n!) * (1! + 2! + 3! + ... + n!) for a given n, where n is an integer greater or equal to 1.
Your result should be within 10^-6 of the expected one.
Remark
Keep in mind that factorials grow rather rapidly, and you need to handle large inputs.
Hint
You could try to simplify the expression.

View File

@@ -0,0 +1,36 @@
export function factorial(n: number): number {
let result = n;
for (let i = n - 1; i > 0; i--) {
result *= i;
}
return result;
}
// inclusive both sides
export function partialFactorial(to: number, from: number): number {
if (to === from) return 1;
if (to > from) {
throw new Error('to must be less than from');
}
let result = from;
for (let i = from - 1; i >= to; i--) {
result *= i;
}
return result;
}
export function going(n: number): number {
// your code
let result = 1 / n;
for (let i = 2; i <= n; i++) {
result += 1 / partialFactorial(i, n);
}
return result;
}

View File

@@ -0,0 +1,14 @@
import {going} from './solution'
import {assert} from "chai";
function testing(n:number, expected:number) {
assert.approximately(going(n), expected, 1e-6)
}
describe("Fixed Tests going", function() {
it("Basic tests", function() {
testing(5, 1.275);
testing(6, 1.2125);
testing(7, 1.173214);
});
});