diff --git a/src/exo7/exo7.test.ts b/src/exo7/exo7.test.ts new file mode 100644 index 0000000..aa583d0 --- /dev/null +++ b/src/exo7/exo7.test.ts @@ -0,0 +1,89 @@ +import { + allPageViews, + intersectionPageViews, + mapWithConcatenatedEntries, + mapWithLastEntry, + nonPrimeOdds, + numberArray, + numberArrayFromSet, + numberSet, + primeOdds, +} from './exo7'; + +describe('exo7', () => { + describe('numberSet', () => { + it('should be the set of unique values from `numberArray`', () => { + expect(numberSet).toStrictEqual(new Set(numberArray)); + }); + }); + + describe('numberArrayFromSet', () => { + it('should be the array of unique values from `numberArray`', () => { + expect(numberArrayFromSet).toStrictEqual( + [...new Set(numberArray)].sort((a, b) => a - b), + ); + }); + }); + + describe('mapWithLastEntry', () => { + it('should construct the map from `associativeArray` keeping only the last entry for colliding keys', () => { + expect(mapWithLastEntry).toStrictEqual( + new Map([ + [1, 'Alice'], + [3, 'Clara'], + [4, 'Denise'], + [2, 'Robert'], + ]), + ); + }); + }); + + describe('mapWithConcatenatedEntries', () => { + it('should construct the map from `associativeArray` concatenating values for colliding keys', () => { + expect(mapWithConcatenatedEntries).toStrictEqual( + new Map([ + [1, 'Alice'], + [3, 'Clara'], + [4, 'Denise'], + [2, 'BobRobert'], + ]), + ); + }); + }); + + describe('nonPrimeOdds', () => { + it('should contain only the odd numbers that are not prime', () => { + expect(nonPrimeOdds).toStrictEqual(new Set([1, 9])); + }); + }); + + describe('primeOdds', () => { + it('should contain only the odd numbers that are also prime', () => { + expect(primeOdds).toStrictEqual(new Set([3, 5, 7])); + }); + }); + + describe('allPageViews', () => { + it('should contain the map of aggregated page views from both sources of analytics', () => { + expect(allPageViews).toStrictEqual( + new Map([ + ['home', { page: 'home', views: 15 }], + ['about', { page: 'about', views: 2 }], + ['blog', { page: 'blog', views: 42 }], + ['faq', { page: 'faq', views: 5 }], + ]), + ); + }); + }); + + describe('intersectionPageViews', () => { + it('should contain the map of intersecting page views from both sources of analytics', () => { + expect(intersectionPageViews).toStrictEqual( + new Map([ + ['home', { page: 'home', views: 15 }], + ['blog', { page: 'blog', views: 42 }], + ]), + ); + }); + }); +}); diff --git a/src/exo7/exo7.ts b/src/exo7/exo7.ts new file mode 100644 index 0000000..e5a3915 --- /dev/null +++ b/src/exo7/exo7.ts @@ -0,0 +1,187 @@ +// `fp-ts` training Exercise 7 +// Manipulate collections with type-classes + +import { unimplemented } from '../utils'; + +// In this exercise, we will learn how to manipulate essential collections +// such as `Set` and `Map`. +// +// These collections have very important properties that make them slightly +// more complex than the venerable `Array`: +// - `Set` requires each element to be unique +// - `Map` associates unique keys to values +// +// In fact, it can sometimes be helpful to think of `Set` as a special case +// of `Map` where `Set` is strictly equivalent to `Map`. +// +// To manipulate these collections, we need often need to inform `fp-ts` on +// how to uphold the properties outlined above (eg. how to determine whether +// two elements or keys have the same value, how to combine values together +// in case of key collision or how to order the values when converting back +// to an array). +// +// And the way to describe those properties for the specific inner types of a +// given `Set` or `Map` is... TYPECLASSES! + +/////////////////////////////////////////////////////////////////////////////// +// SET // +/////////////////////////////////////////////////////////////////////////////// + +// A `Set` is pretty straightforward, it stores values but doesn't care at all +// about the ordering of those values. Furthermore, it ignores duplicates. + +export const numberArray: ReadonlyArray = [7, 42, 1337, 1, 0, 1337, 42]; + +// Construct `numberSet` from the provided `numberArray`. +// You need to use the `ReadonlySet` module from `fp-ts` instead of the +// JavaScript standard constructor. +// +// HINTS: +// - You can look into `readonlySet.fromReadonlyArray` +// - `fp-ts` doesn't know how you want to define equality for the inner type +// and requires you to provide an `Eq` instance + +export const numberSet: ReadonlySet = unimplemented(); + +// Convert `numberSet` back to an an array in `numberArrayFromSet`. +// You need to use the `ReadonlySet` module from `fp-ts` instead of the +// JavaScript standard constructor. +// +// HINTS: +// - You can look into `readonlySet.toReadonlyArray` +// - The elements in `numberSet` have no guarantees whatsoever regarding +// their ordering. This ordering could be totally random. But remember that +// functional programming is all about purity. Converting a set to an array +// is a pure operation and as such, should return the same value for the +// same input. This means you **need** to instruct `fp-ts` on how you wish +// the values to be ordered in the output array, by providing an `Ord` +// instance. + +export const numberArrayFromSet: ReadonlyArray = unimplemented(); + +/////////////////////////////////////////////////////////////////////////////// +// MAP // +/////////////////////////////////////////////////////////////////////////////// + +// A `Map` associates a set of unique keys to arbitrary values. Values +// themselves can be duplicated across various keys but keys have to be unique. +// +// This means than when constructing a `Map` from an `Array`, you need to be +// explicit on how you wish to combine values in case of key collision (maybe +// you want to only insert the last value provided, maybe the first, maybe you +// want to combine both values in a specific way eg. concatenate strings, add +// numbers, etc...) + +export const associativeArray: ReadonlyArray<[number, string]> = [ + [1, 'Alice'], + [2, 'Bob'], + [3, 'Clara'], + [4, 'Denise'], + [2, 'Robert'], +]; + +// Construct `mapWithLastEntry` from the provided `associativeArray`. +// You need to use the `ReadonlyMap` module from `fp-ts` instead of the +// JavaScript standard constructor. +// +// The resulting `Map` should have the following shape: +// 1 => 'Alice' +// 2 => 'Robert' +// 3 => 'Clara' +// 4 => 'Denise' +// +// HINTS: +// - You can look into `readonlyMap.fromFoldable` +// - You need to provide an `Eq` instance for the key type +// - You need to provide a `Magma` instance for the value type. In this case, +// the `Magma` instance should ignore the first value and return the second. +// (You can define your own, or look into the `Magma` or `Semigroup` module) +// - You need to provide the `Foldable` instance for the input container type. +// Just know that you can construct a `Map` from other types than `Array` as +// long as they implement `Foldable`. Here, you can simply pass the standard +// `readonlyArray.Foldable` instance. + +export const mapWithLastEntry: ReadonlyMap = unimplemented(); + +// Same thing as above, except that upon key collision we don't want to simply +// select the newest entry value but append it to the previous one. +// +// Basically, the resulting `Map` here should have the following shape: +// 1 => 'Alice' +// 2 => 'BobRobert' +// 3 => 'Clara' +// 4 => 'Denise' +// +// HINT: +// - You can look into the `Semigroup` typeclass as it is a super-class of +// `Magma`, meaning that a `Semigroup` is also necessarily a `Magma`. +// The `string` module may contain what you need ;) +// +// Bonus point: +// Did you find something helpful in the `Semigroup` module that may have been +// helpful in defining `mapWithLastEntry`? + +export const mapWithConcatenatedEntries: ReadonlyMap = + unimplemented(); + +/////////////////////////////////////////////////////////////////////////////// +// DIFFERENCE / UNION / INTERSECTION // +/////////////////////////////////////////////////////////////////////////////// + +export const primes = new Set([2, 3, 5, 7]); +export const odds = new Set([1, 3, 5, 7, 9]); + +// Construct the set `nonPrimeOdds` from the two sets defined above. It should +// only include the odd numbers that are not prime. +// +// HINT: +// - Be mindful of the order of operands for the operator you will chose. + +export const nonPrimeOdds: ReadonlySet = unimplemented(); + +// Construct the set `primeOdds` from the two sets defined above. It should +// only include the odd numbers that are also prime. + +export const primeOdds: ReadonlySet = unimplemented(); + +/////////////////////////////////////////////////////////////////////////////// + +export type Analytics = { + page: string; + views: number; +}; + +// These example maps are voluntarily written in a non fp-ts way to not give +// away too much obviously ;) +// +// As an exercise for the reader, they may rewrite those with what they've +// learned earlier. + +export const pageViewsA = new Map( + [ + { page: 'home', views: 5 }, + { page: 'about', views: 2 }, + { page: 'blog', views: 7 }, + ].map(entry => [entry.page, entry]), +); + +export const pageViewsB = new Map( + [ + { page: 'home', views: 10 }, + { page: 'blog', views: 35 }, + { page: 'faq', views: 5 }, + ].map(entry => [entry.page, entry]), +); + +// Construct the map with the total page views for all the pages in both sources +// of analytics `pageViewsA` and `pageViewsB`. +// +// In case a page appears in both sources, their view count should be summed. + +export const allPageViews: ReadonlyMap = unimplemented(); + +// Construct the map with the total page views but only for the pages that +// appear in both sources of analytics `pageViewsA` and `pageViewsB`. + +export const intersectionPageViews: ReadonlyMap = + unimplemented(); diff --git a/src/exo7/index.ts b/src/exo7/index.ts new file mode 100644 index 0000000..95ff5eb --- /dev/null +++ b/src/exo7/index.ts @@ -0,0 +1 @@ +export * from './exo7';