From 109b18cbd7396db768d99ab54507e0d1f2a13ba3 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:37:38 +0200 Subject: [PATCH 01/14] Exercise 00: Solution. --- exercises/exercise-00/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-00/index.ts b/exercises/exercise-00/index.ts index 4b815840..85e15366 100644 --- a/exercises/exercise-00/index.ts +++ b/exercises/exercise-00/index.ts @@ -26,7 +26,13 @@ Run this exercise: */ -const users: unknown[] = [ +interface User { + name: string; + age: number; + occupation: string; +} + +const users: User[] = [ { name: 'Max Mustermann', age: 25, @@ -39,7 +45,7 @@ const users: unknown[] = [ } ]; -function logPerson(user: unknown) { +function logPerson(user: User) { console.log(` - ${chalk.green(user.name)}, ${user.age}`); } From 824e81d2e54c7861217ffe4b890a022c47efcd29 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:38:25 +0200 Subject: [PATCH 02/14] Exercise 01: Solution. --- exercises/exercise-01/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-01/index.ts b/exercises/exercise-01/index.ts index 6970e2a7..95d75f70 100644 --- a/exercises/exercise-01/index.ts +++ b/exercises/exercise-01/index.ts @@ -40,7 +40,9 @@ interface Admin { role: string; } -const persons: User[] /* <- Person[] */ = [ +type Person = User | Admin; + +const persons: Person[] = [ { name: 'Max Mustermann', age: 25, @@ -63,7 +65,7 @@ const persons: User[] /* <- Person[] */ = [ } ]; -function logPerson(user: User) { +function logPerson(user: Person) { console.log(` - ${chalk.green(user.name)}, ${user.age}`); } From 062ee381d2e0eda28e8eb550d7102a4943c06499 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:39:05 +0200 Subject: [PATCH 03/14] Exercise 02: Solution. --- exercises/exercise-02/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/exercise-02/index.ts b/exercises/exercise-02/index.ts index c28bfa4e..0e8ff05d 100644 --- a/exercises/exercise-02/index.ts +++ b/exercises/exercise-02/index.ts @@ -65,7 +65,7 @@ const persons: Person[] = [ function logPerson(person: Person) { let additionalInformation: string; - if (person.role) { + if ('role' in person) { additionalInformation = person.role; } else { additionalInformation = person.occupation; From 52395e99e1885edc89e25be8293d4a8d8afb07ea Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:40:09 +0200 Subject: [PATCH 04/14] Exercise 03: Solution. --- exercises/exercise-03/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-03/index.ts b/exercises/exercise-03/index.ts index 6a7c911c..3efb5a5d 100644 --- a/exercises/exercise-03/index.ts +++ b/exercises/exercise-03/index.ts @@ -48,11 +48,11 @@ const persons: Person[] = [ { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' } ]; -function isAdmin(person: Person) { +function isAdmin(person: Person): person is Admin { return person.type === 'admin'; } -function isUser(person: Person) { +function isUser(person: Person): person is User { return person.type === 'user'; } From df0acda1a354be8c325d3e62cfff7cb8138693d8 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:43:48 +0200 Subject: [PATCH 05/14] Exercise 04: Solution. --- exercises/exercise-04/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-04/index.ts b/exercises/exercise-04/index.ts index fd114b0e..51d55eab 100644 --- a/exercises/exercise-04/index.ts +++ b/exercises/exercise-04/index.ts @@ -95,9 +95,11 @@ function logPerson(person: Person) { console.log(` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`); } -function filterUsers(persons: Person[], criteria: User): User[] { +type FilterUsersCriteria = Partial>; + +function filterUsers(persons: Person[], criteria: FilterUsersCriteria): User[] { return persons.filter(isUser).filter((user) => { - let criteriaKeys = Object.keys(criteria) as (keyof User)[]; + let criteriaKeys = Object.keys(criteria) as (keyof FilterUsersCriteria)[]; return criteriaKeys.every((fieldName) => { return user[fieldName] === criteria[fieldName]; }); From ae84796be6123a82ebbe0aa007c4800ec9b19d80 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:49:06 +0200 Subject: [PATCH 06/14] Exercise 05: Solution. --- exercises/exercise-05/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-05/index.ts b/exercises/exercise-05/index.ts index 45e349ae..06d1287b 100644 --- a/exercises/exercise-05/index.ts +++ b/exercises/exercise-05/index.ts @@ -64,11 +64,19 @@ function logPerson(person: Person) { ); } -function filterPersons(persons: Person[], personType: string, criteria: unknown): unknown[] { +function getObjectKeys(obj: O): (keyof O)[] { + return Object.keys(obj) as (keyof O)[]; +} + +type FilterCriteria = Partial>; + +function filterPersons(persons: Person[], personType: 'admin', criteria: FilterCriteria): Admin[]; +function filterPersons(persons: Person[], personType: 'user', criteria: FilterCriteria): User[]; +function filterPersons(persons: Person[], personType: 'admin' | 'user', criteria: FilterCriteria): Person[] { return persons .filter((person) => person.type === personType) .filter((person) => { - let criteriaKeys = Object.keys(criteria) as (keyof Person)[]; + let criteriaKeys = getObjectKeys(criteria); return criteriaKeys.every((fieldName) => { return person[fieldName] === criteria[fieldName]; }); From a1f3cb8d90910dcc955f7789755f95025a411415 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:50:32 +0200 Subject: [PATCH 07/14] Exercise 06: Solution. --- exercises/exercise-06/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/exercise-06/index.ts b/exercises/exercise-06/index.ts index cf186c13..cb293f9c 100644 --- a/exercises/exercise-06/index.ts +++ b/exercises/exercise-06/index.ts @@ -88,7 +88,7 @@ const users: User[] = [ } ]; -function swap(v1, v2) { +function swap(v1: T1, v2: T2): [T2, T1] { return [v2, v1]; } From 284ed6d60d8a7fce50783e50c7163c723c4e1a33 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:52:25 +0200 Subject: [PATCH 08/14] Exercise 07: Solution. --- exercises/exercise-07/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/exercise-07/index.ts b/exercises/exercise-07/index.ts index 7b77e571..553f557a 100644 --- a/exercises/exercise-07/index.ts +++ b/exercises/exercise-07/index.ts @@ -41,7 +41,9 @@ interface Admin { role: string; } -type PowerUser = unknown; +type PowerUser = Omit & { + type: 'powerUser' +}; type Person = User | Admin | PowerUser; From d07076675fd0e378fd2877e3a9531f3f21dfac2e Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 11:56:40 +0200 Subject: [PATCH 09/14] Exercise 08: Solution. --- exercises/exercise-08/index.ts | 37 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/exercises/exercise-08/index.ts b/exercises/exercise-08/index.ts index 1638eddf..a5414b90 100644 --- a/exercises/exercise-08/index.ts +++ b/exercises/exercise-08/index.ts @@ -63,50 +63,39 @@ const users: User[] = [ { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' } ]; -type AdminsApiResponse = ( - { - status: 'success'; - data: Admin[]; - } | - { - status: 'error'; - error: string; - } +type ApiResponse = ( + { + status: 'success'; + data: T; + } | + { + status: 'error'; + error: string; + } ); -function requestAdmins(callback: (response: AdminsApiResponse) => void) { +function requestAdmins(callback: (response: ApiResponse) => void) { callback({ status: 'success', data: admins }); } -type UsersApiResponse = ( - { - status: 'success'; - data: User[]; - } | - { - status: 'error'; - error: string; - } -); - -function requestUsers(callback: (response: UsersApiResponse) => void) { +function requestUsers(callback: (response: ApiResponse) => void) { callback({ status: 'success', data: users }); } -function requestCurrentServerTime(callback: (response: unknown) => void) { +function requestCurrentServerTime(callback: (response: ApiResponse) => void) { callback({ status: 'success', data: Date.now() }); } -function requestCoffeeMachineQueueLength(callback: (response: unknown) => void) { +function requestCoffeeMachineQueueLength(callback: (response: ApiResponse) => void) { callback({ status: 'error', error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.' From fc6755e4720cdfa0e2187b1cf71ea9b457bcb8ec Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 12:01:32 +0200 Subject: [PATCH 10/14] Exercise 09: Solution. --- exercises/exercise-09/index.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-09/index.ts b/exercises/exercise-09/index.ts index 66607dfc..38bb95eb 100644 --- a/exercises/exercise-09/index.ts +++ b/exercises/exercise-09/index.ts @@ -71,8 +71,19 @@ type ApiResponse = ( } ); -function promisify(arg: unknown): unknown { - return null; +type PromisifyOldFunctionDefinition = (callback: (response: ApiResponse) => void) => void; +type PromisifyNewFunctionDefinition = () => Promise; + +function promisify(oldFunction: PromisifyOldFunctionDefinition): PromisifyNewFunctionDefinition { + return () => new Promise((resolve, reject) => { + oldFunction((response) => { + if (response.status === 'error') { + reject(new Error(response.error)); + return; + } + resolve(response.data); + }); + }); } const oldApi = { From 68363742a6d3a513e725a050234e2cbff927a733 Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 12:07:47 +0200 Subject: [PATCH 11/14] Exercise 10: Solution. --- exercises/exercise-10/declarations/str-utils/index.d.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/exercises/exercise-10/declarations/str-utils/index.d.ts b/exercises/exercise-10/declarations/str-utils/index.d.ts index 53567493..4deded1f 100644 --- a/exercises/exercise-10/declarations/str-utils/index.d.ts +++ b/exercises/exercise-10/declarations/str-utils/index.d.ts @@ -1,4 +1,9 @@ declare module 'str-utils' { - // export const ... - // export function ... + type StrUtil = (input: string) => string; + + export const strReverse: StrUtil; + export const strToLower: StrUtil; + export const strToUpper: StrUtil; + export const strRandomize: StrUtil; + export const strInvertCase: StrUtil; } From d200a2e52d725dd33ea8a32ee1f717b5c589c2dc Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 12:17:06 +0200 Subject: [PATCH 12/14] Exercise 11: Solution. --- exercises/exercise-11/declarations/stats/index.d.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/exercises/exercise-11/declarations/stats/index.d.ts b/exercises/exercise-11/declarations/stats/index.d.ts index da99fbfc..f8747df8 100644 --- a/exercises/exercise-11/declarations/stats/index.d.ts +++ b/exercises/exercise-11/declarations/stats/index.d.ts @@ -1,3 +1,13 @@ declare module 'stats' { - export function getMaxIndex(input: unknown, comparator: unknown): unknown; + type Comparator = (a: I, b: I) => number; + type StatIndexFunction = (input: I[], comparator: Comparator) => number; + type StatElementFunction = (input: I[], comparator: Comparator) => I; + + export const getMaxIndex: StatIndexFunction; + export const getMinIndex: StatIndexFunction; + export const getMedianIndex: StatIndexFunction; + export const getMaxElement: StatElementFunction; + export const getMinElement: StatElementFunction; + export const getMedianElement: StatElementFunction; + export const getAverageValue: (items: I[], getValue: (item: I) => O) => O; } From 1a6ade1088c22c839ed831e8e4d793585dcc82dd Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 12:21:34 +0200 Subject: [PATCH 13/14] Exercise 12: Solution. --- .../exercise-12/module-augmentations/date-wizard/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/exercises/exercise-12/module-augmentations/date-wizard/index.ts b/exercises/exercise-12/module-augmentations/date-wizard/index.ts index 8355c24a..23e942e0 100644 --- a/exercises/exercise-12/module-augmentations/date-wizard/index.ts +++ b/exercises/exercise-12/module-augmentations/date-wizard/index.ts @@ -2,5 +2,10 @@ import 'date-wizard'; declare module 'date-wizard' { - // Add your module extensions here. + interface DateDetails { + hours: number; + minutes: number; + seconds: number; + } + export function pad(num: number): string; } From ba6905b0a5b69cd4d08ee7bb0b9fb1824b7ed71e Mon Sep 17 00:00:00 2001 From: Marat Dulin Date: Thu, 30 Apr 2020 14:03:26 +0200 Subject: [PATCH 14/14] Exercise 13: Solution. --- exercises/exercise-13/database.ts | 170 +++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 4 deletions(-) diff --git a/exercises/exercise-13/database.ts b/exercises/exercise-13/database.ts index 9f938b85..47ead706 100644 --- a/exercises/exercise-13/database.ts +++ b/exercises/exercise-13/database.ts @@ -1,13 +1,175 @@ +import {readFile} from 'fs'; + +type FieldQuery = + | {$eq: FT} + | {$gt: FT} + | {$lt: FT} + | {$in: FT[]}; + +type Query = {[K in keyof T]?: FieldQuery} & { + $text?: string; + $and?: Query[]; + $or?: Query[]; +}; + +interface Documents { + [key: string]: boolean; +} + +function intersectSearchResults(documents: Documents[]) { + const result: Documents = {}; + if (documents.length === 0) { + return result; + } + for (let key of Object.keys(documents[0])) { + let keep = true; + for (let i = 1; i < documents.length; i++) { + if (!documents[i][key]) { + keep = false; + break; + } + } + if (keep) { + result[key] = true; + } + } + return result; +} + +function mergeSearchResults(documents: Documents[]) { + const result: Documents = {}; + for (const document of documents) { + for (const key of Object.keys(document)) { + result[key] = true; + } + } + return result; +} + export class Database { protected filename: string; - protected fullTextSearchFieldNames: unknown[]; + protected fullTextSearchFieldNames: (keyof T)[]; + protected getDocumentsPromise: Promise | null = null; + protected getFullTextSearchIndexPromise: Promise | null = null; - constructor(filename: string, fullTextSearchFieldNames) { + constructor(filename: string, fullTextSearchFieldNames: (keyof T)[]) { this.filename = filename; this.fullTextSearchFieldNames = fullTextSearchFieldNames; } - async find(query): Promise { - return []; + async find(query: Query): Promise { + const documents = await this.getDocuments(); + return Object.keys(await this.findMatchingDocuments(query)) + .map(Number) + .map((index) => documents[index]); + } + + protected getDocuments() { + return this.getDocumentsPromise || (this.getDocumentsPromise = new Promise((resolve, reject) => { + readFile(this.filename, 'utf8', (error, data) => { + if (error) { + reject(error); + return; + } + resolve( + data + .trim() + .split('\n') + .filter((line) => line[0] === 'E') + .map((line) => JSON.parse(line.substr(1))) + ); + }); + })); + } + + protected getFullTextSearchIndex() { + return this.getFullTextSearchIndexPromise || ( + this.getFullTextSearchIndexPromise = this.getDocuments().then((documents) => { + const fullTextSearchIndex = new FullTextSearchIndex(); + documents.forEach((document, id) => { + fullTextSearchIndex.addDocument( + id, + this.fullTextSearchFieldNames.map((key) => String(document[key])) + ); + }); + return fullTextSearchIndex; + }) + ); + } + + protected async getMatchingDocumentIds(comparator: (document: T) => boolean) { + const result: Documents = {}; + const documents = await this.getDocuments(); + for (let i = 0; i < documents.length; i++) { + if (comparator(documents[i])) { + result[i] = true; + } + } + return result; + } + + protected async findMatchingDocuments(query: Query): Promise { + const result: Documents[] = []; + for (const key of Object.keys(query) as (keyof Query)[]) { + if (key === '$text') { + result.push((await this.getFullTextSearchIndex()).search(query.$text!)) + } else if (key === '$and') { + result.push( + intersectSearchResults(await Promise.all(query.$and!.map(this.findMatchingDocuments, this))) + ); + } else if (key === '$or') { + result.push(mergeSearchResults(await Promise.all(query.$or!.map(this.findMatchingDocuments, this)))); + } else { + const fieldQuery = query[key] as FieldQuery; + if ('$eq' in fieldQuery) { + result.push(await this.getMatchingDocumentIds((document) => document[key] === fieldQuery.$eq)); + } else if ('$gt' in fieldQuery) { + result.push( + await this.getMatchingDocumentIds((document) => Number(document[key]) > Number(fieldQuery.$gt)) + ); + } else if ('$lt' in fieldQuery) { + result.push( + await this.getMatchingDocumentIds((document) => Number(document[key]) < Number(fieldQuery.$lt)) + ); + } else if ('$in' in fieldQuery) { + const index: {[key: string]: boolean} = {}; + for (const val of fieldQuery.$in) { + index[String(val)] = true; + } + result.push( + await this.getMatchingDocumentIds((document) => index.hasOwnProperty(String(document[key]))) + ); + } else { + throw new Error('Incorrect query.'); + } + } + } + return intersectSearchResults(result); + } +} + +class FullTextSearchIndex { + protected wordsToDocuments: {[word: string]: Documents} = {}; + + protected breakTextIntoWords(text: string) { + return text.toLowerCase().replace(/\W+/g, ' ').trim().split(' '); + } + + addDocument(document: number, texts: string[]) { + for (const text of texts) { + const words = this.breakTextIntoWords(text); + for (let word of words) { + this.wordsToDocuments[word] = this.wordsToDocuments[word] || {}; + this.wordsToDocuments[word][document] = true; + } + } + } + + search(query: string): Documents { + return intersectSearchResults( + this.breakTextIntoWords(query) + .map((word) => this.wordsToDocuments[word]) + .filter(Boolean) + ); } }