Compare commits
13 Commits
69e937d98e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 897f802659 | |||
| 5f7c1ca61a | |||
| 69edf86b63 | |||
| 857a252fea | |||
| b3ac5ee075 | |||
| 3ffb7f8181 | |||
| 03bdd63342 | |||
| 849d003a68 | |||
| ba4c999a99 | |||
| 2ea2410fb3 | |||
| ca92878a0a | |||
| 58ce2189f7 | |||
| b768376ff0 |
261
1kyu/functional-sql/description.md
Normal file
261
1kyu/functional-sql/description.md
Normal 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).
|
||||
232
1kyu/functional-sql/solution.ts
Normal file
232
1kyu/functional-sql/solution.ts
Normal 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;
|
||||
};
|
||||
632
1kyu/functional-sql/tests.ts
Normal file
632
1kyu/functional-sql/tests.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
41
2kyu/evaluate-mathematical-expression/description.md
Normal file
41
2kyu/evaluate-mathematical-expression/description.md
Normal 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.
|
||||
359
2kyu/evaluate-mathematical-expression/solution.ts
Normal file
359
2kyu/evaluate-mathematical-expression/solution.ts
Normal 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;
|
||||
}
|
||||
117
2kyu/evaluate-mathematical-expression/tests.ts
Normal file
117
2kyu/evaluate-mathematical-expression/tests.ts
Normal 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'
|
||||
}]);
|
||||
});
|
||||
});
|
||||
26
4kyu/smallest-possible-sum/description.md
Normal file
26
4kyu/smallest-possible-sum/description.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Description
|
||||
Given an array X of positive integers, its elements are to be transformed by running the following operation on them as many times as required:
|
||||
|
||||
if X[i] > X[j] then X[i] = X[i] - X[j]
|
||||
|
||||
When no more transformations are possible, return its sum ("smallest possible sum").
|
||||
|
||||
For instance, the successive transformation of the elements of input X = [6, 9, 21] is detailed below:
|
||||
|
||||
X_1 = [6, 9, 12] # -> X_1[2] = X[2] - X[1] = 21 - 9
|
||||
X_2 = [6, 9, 6] # -> X_2[2] = X_1[2] - X_1[0] = 12 - 6
|
||||
X_3 = [6, 3, 6] # -> X_3[1] = X_2[1] - X_2[0] = 9 - 6
|
||||
X_4 = [6, 3, 3] # -> X_4[2] = X_3[2] - X_3[1] = 6 - 3
|
||||
X_5 = [3, 3, 3] # -> X_5[1] = X_4[0] - X_4[1] = 6 - 3
|
||||
The returning output is the sum of the final transformation (here 9).
|
||||
|
||||
Example
|
||||
solution([6, 9, 21]) #-> 9
|
||||
Solution steps:
|
||||
[6, 9, 12] #-> X[2] = 21 - 9
|
||||
[6, 9, 6] #-> X[2] = 12 - 6
|
||||
[6, 3, 6] #-> X[1] = 9 - 6
|
||||
[6, 3, 3] #-> X[2] = 6 - 3
|
||||
[3, 3, 3] #-> X[1] = 6 - 3
|
||||
Additional notes:
|
||||
There are performance tests consisted of very big numbers and arrays of size at least 30000. Please write an efficient algorithm to prevent timeout.
|
||||
59
4kyu/smallest-possible-sum/solution.ts
Normal file
59
4kyu/smallest-possible-sum/solution.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// idea: do not ever sort
|
||||
// simply keep looping from left to right and back to left
|
||||
// whenever a smaller number is found,
|
||||
// subtract as many multiples of that number from the rest of the array
|
||||
|
||||
const subtractMultiples = (a: number, b: number): number => {
|
||||
// subtract a from b as many times as possible
|
||||
const mod = b % a;
|
||||
if (mod === 0) {
|
||||
// figure out how many times a can be subtracted from b
|
||||
const div = b / a - 1;
|
||||
return b - a * div;
|
||||
} else {
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
|
||||
const operate = (arr: number[]): boolean => {
|
||||
|
||||
let step = 1;
|
||||
let i = 0;
|
||||
let done = false;
|
||||
let hasChangedDuringLoop = false;
|
||||
while (!done) {
|
||||
if (i + step >= arr.length || i + step < 0) {
|
||||
// reverse direction
|
||||
step = -step;
|
||||
if (!hasChangedDuringLoop) {
|
||||
done = true;
|
||||
}
|
||||
hasChangedDuringLoop = false;
|
||||
}
|
||||
|
||||
const first = arr[i];
|
||||
const next = arr[i + step];
|
||||
|
||||
if (first > next) {
|
||||
arr[i] = subtractMultiples(next, first);
|
||||
hasChangedDuringLoop = true;
|
||||
} else if (first < next) {
|
||||
arr[i + step] = subtractMultiples(first, next);
|
||||
hasChangedDuringLoop = true;
|
||||
}
|
||||
|
||||
i += step;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function solution(numbers: number[]): number {
|
||||
let done = false;
|
||||
while (!done) {
|
||||
done = operate(numbers);
|
||||
}
|
||||
|
||||
// sum the numbers
|
||||
return numbers.reduce((acc, num) => acc + num, 0);
|
||||
}
|
||||
16
4kyu/smallest-possible-sum/tests.ts
Normal file
16
4kyu/smallest-possible-sum/tests.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// See https://www.chaijs.com for how to use Chai.
|
||||
import { assert } from "chai";
|
||||
|
||||
import { solution } from "./solution";
|
||||
|
||||
describe('Example test', function () {
|
||||
it('[6,9,12]', () => assert.strictEqual(solution([6, 9, 21]), 9));
|
||||
it('[1,21,55]', () => assert.strictEqual(solution([1, 21, 55]), 3));
|
||||
it('[3,13,23,7,83]', () => assert.strictEqual(solution([3, 13, 23, 7, 83]), 5));
|
||||
it('[4,16,24]', () => assert.strictEqual(solution([4, 16, 24]), 12));
|
||||
it('[30,12]', () => assert.strictEqual(solution([30, 12]), 12));
|
||||
it('[60,12,96,48,60,24,72,36,72,72,48]', () => assert.strictEqual(solution([60, 12, 96, 48, 60, 24, 72, 36, 72, 72, 48]), 132));
|
||||
it('[71,71,71,71,71,71,71,71,71,71,71,71,71]', () => assert.strictEqual(solution([71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71]), 71 * 13));
|
||||
it('[11,22]', () => assert.strictEqual(solution([11,22]), 22));
|
||||
it('[9]', () => assert.strictEqual(solution([9]), 9));
|
||||
});
|
||||
21
5kyu/going-to-zero-or-to-infinity/description.md
Normal file
21
5kyu/going-to-zero-or-to-infinity/description.md
Normal 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.
|
||||
36
5kyu/going-to-zero-or-to-infinity/solution.ts
Normal file
36
5kyu/going-to-zero-or-to-infinity/solution.ts
Normal 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;
|
||||
}
|
||||
14
5kyu/going-to-zero-or-to-infinity/tests.ts
Normal file
14
5kyu/going-to-zero-or-to-infinity/tests.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user