From 41ec39c9616f32bf53b30fd0c34f01d168a8f5be Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Wed, 4 Jun 2025 17:25:35 +0200 Subject: [PATCH 01/10] extract db --- parts/4/blogApp/src/app.js | 14 ++------------ parts/4/blogApp/src/db.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 parts/4/blogApp/src/db.js diff --git a/parts/4/blogApp/src/app.js b/parts/4/blogApp/src/app.js index 170b6ad..b6d10e1 100644 --- a/parts/4/blogApp/src/app.js +++ b/parts/4/blogApp/src/app.js @@ -1,19 +1,9 @@ const express = require('express') -const mongoose = require('mongoose') - +const db = require('./db') const app = express() -const blogSchema = mongoose.Schema({ - title: String, - author: String, - url: String, - likes: Number, -}) +const Blog = db.Blog -const Blog = mongoose.model('Blog', blogSchema) - -const mongoUrl = 'mongodb://localhost/bloglist' -mongoose.connect(mongoUrl) app.use(express.json()) diff --git a/parts/4/blogApp/src/db.js b/parts/4/blogApp/src/db.js new file mode 100644 index 0000000..d288a1d --- /dev/null +++ b/parts/4/blogApp/src/db.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose') + +const blogSchema = mongoose.Schema({ + title: String, + author: String, + url: String, + likes: Number, +}) + +const Blog = mongoose.model('Blog', blogSchema) + +const mongoUrl = 'mongodb://localhost/bloglist' +mongoose.connect(mongoUrl) + +module.exports = {Blog} \ No newline at end of file From 09a320a5da15152d5f3b5b2609a138917a2d0af5 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Wed, 4 Jun 2025 17:44:52 +0200 Subject: [PATCH 02/10] refactor --- parts/4/blogApp/src/app.js | 35 +++++++++++++++++------------------ parts/4/blogApp/src/db.js | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/parts/4/blogApp/src/app.js b/parts/4/blogApp/src/app.js index b6d10e1..a6921bf 100644 --- a/parts/4/blogApp/src/app.js +++ b/parts/4/blogApp/src/app.js @@ -1,27 +1,26 @@ -const express = require('express') -const db = require('./db') -const app = express() +const express = require("express"); +const { models } = require("./db"); +const app = express(); -const Blog = db.Blog +const Blog = models.Blog; +app.use(express.json()); -app.use(express.json()) - -app.get('/api/blogs', (request, response) => { +app.get("/api/blogs", (request, response) => { Blog.find({}).then((blogs) => { - response.json(blogs) - }) -}) + response.json(blogs); + }); +}); -app.post('/api/blogs', (request, response) => { - const blog = new Blog(request.body) +app.post("/api/blogs", (request, response) => { + const blog = new Blog(request.body); blog.save().then((result) => { - response.status(201).json(result) - }) -}) + response.status(201).json(result); + }); +}); -const PORT = 3003 +const PORT = 3003; app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`) -}) \ No newline at end of file + console.log(`Server running on port ${PORT}`); +}); diff --git a/parts/4/blogApp/src/db.js b/parts/4/blogApp/src/db.js index d288a1d..5456522 100644 --- a/parts/4/blogApp/src/db.js +++ b/parts/4/blogApp/src/db.js @@ -12,4 +12,4 @@ const Blog = mongoose.model('Blog', blogSchema) const mongoUrl = 'mongodb://localhost/bloglist' mongoose.connect(mongoUrl) -module.exports = {Blog} \ No newline at end of file +module.exports = {models: {Blog}} \ No newline at end of file From 0d97a86ac8de921076e872782c5e4bb1de9f2093 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Wed, 4 Jun 2025 17:49:14 +0200 Subject: [PATCH 03/10] more refactor --- parts/4/blogApp/src/app.js | 18 ++---------------- parts/4/blogApp/src/routes.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 parts/4/blogApp/src/routes.js diff --git a/parts/4/blogApp/src/app.js b/parts/4/blogApp/src/app.js index a6921bf..b1bb5ca 100644 --- a/parts/4/blogApp/src/app.js +++ b/parts/4/blogApp/src/app.js @@ -1,24 +1,10 @@ const express = require("express"); -const { models } = require("./db"); +const { addRoutes } = require("./routes"); const app = express(); -const Blog = models.Blog; - app.use(express.json()); -app.get("/api/blogs", (request, response) => { - Blog.find({}).then((blogs) => { - response.json(blogs); - }); -}); - -app.post("/api/blogs", (request, response) => { - const blog = new Blog(request.body); - - blog.save().then((result) => { - response.status(201).json(result); - }); -}); +addRoutes(app); const PORT = 3003; app.listen(PORT, () => { diff --git a/parts/4/blogApp/src/routes.js b/parts/4/blogApp/src/routes.js new file mode 100644 index 0000000..7225b3a --- /dev/null +++ b/parts/4/blogApp/src/routes.js @@ -0,0 +1,22 @@ +const { models } = require("./db"); +const Blog = models.Blog; + +BASE_API_PATH = "/api"; + +const addRoutes = (app) => { + app.get(`${BASE_API_PATH}/blogs`, (request, response) => { + Blog.find({}).then((blogs) => { + response.json(blogs); + }); + }); + + app.post(`${BASE_API_PATH}/blogs`, (request, response) => { + const blog = new Blog(request.body); + + blog.save().then((result) => { + response.status(201).json(result); + }); + }); +}; + +module.exports = { addRoutes }; From f58ef53347854beb105cc087d3a4e58aa3fea3a8 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Wed, 4 Jun 2025 17:52:14 +0200 Subject: [PATCH 04/10] completed --- parts/4/notes.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 parts/4/notes.md diff --git a/parts/4/notes.md b/parts/4/notes.md new file mode 100644 index 0000000..d51aac8 --- /dev/null +++ b/parts/4/notes.md @@ -0,0 +1,3 @@ +Exercises: +- [X] 4.1 +- [X] 4.2 From 9bd54f47192d7283fb336826d768a73aa2543fa5 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Wed, 4 Jun 2025 17:58:33 +0200 Subject: [PATCH 05/10] follow along with tests --- parts/4/blogApp/package.json | 2 +- parts/4/blogApp/src/utils.js | 16 ++++++++++++++++ parts/4/blogApp/tests/average.test.js | 18 ++++++++++++++++++ parts/4/blogApp/tests/reverse.test.js | 22 ++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 parts/4/blogApp/src/utils.js create mode 100644 parts/4/blogApp/tests/average.test.js create mode 100644 parts/4/blogApp/tests/reverse.test.js diff --git a/parts/4/blogApp/package.json b/parts/4/blogApp/package.json index 46a1109..66dab5e 100644 --- a/parts/4/blogApp/package.json +++ b/parts/4/blogApp/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "node src/app.js", "dev": "node --watch src/app.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test" }, "author": "", "license": "ISC", diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js new file mode 100644 index 0000000..30f4ac3 --- /dev/null +++ b/parts/4/blogApp/src/utils.js @@ -0,0 +1,16 @@ +const reverse = (string) => { + return string.split("").reverse().join(""); +}; + +const average = (array) => { + const reducer = (sum, item) => { + return sum + item; + }; + + return array.length === 0 ? 0 : array.reduce(reducer, 0) / array.length; +}; + +module.exports = { + reverse, + average, +}; diff --git a/parts/4/blogApp/tests/average.test.js b/parts/4/blogApp/tests/average.test.js new file mode 100644 index 0000000..a7f56f5 --- /dev/null +++ b/parts/4/blogApp/tests/average.test.js @@ -0,0 +1,18 @@ +const { test, describe } = require('node:test') +const assert = require('node:assert') + +const average = require('../src/utils').average + +describe('average', () => { + test('of one value is the value itself', () => { + assert.strictEqual(average([1]), 1) + }) + + test('of many is calculated right', () => { + assert.strictEqual(average([1, 2, 3, 4, 5, 6]), 3.5) + }) + + test('of empty array is zero', () => { + assert.strictEqual(average([]), 0) + }) +}) \ No newline at end of file diff --git a/parts/4/blogApp/tests/reverse.test.js b/parts/4/blogApp/tests/reverse.test.js new file mode 100644 index 0000000..59f044d --- /dev/null +++ b/parts/4/blogApp/tests/reverse.test.js @@ -0,0 +1,22 @@ +const { test } = require('node:test') +const assert = require('node:assert') + +const reverse = require('../src/utils.js').reverse + +test('reverse of a', () => { + const result = reverse('a') + + assert.strictEqual(result, 'a') +}) + +test('reverse of react', () => { + const result = reverse('react') + + assert.strictEqual(result, 'tcaer') +}) + +test('reverse of saippuakauppias', () => { + const result = reverse('saippuakauppias') + + assert.strictEqual(result, 'saippuakauppias') +}) \ No newline at end of file From a41907fe4ba56a66fd7b0cead874d65fa473b6b5 Mon Sep 17 00:00:00 2001 From: counterweight Date: Wed, 4 Jun 2025 23:35:31 +0200 Subject: [PATCH 06/10] exercise 4.3 completed --- parts/4/blogApp/src/utils.js | 6 ++++++ parts/4/blogApp/tests/listHelper.test.js | 10 ++++++++++ parts/4/notes.md | 5 +++-- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 parts/4/blogApp/tests/listHelper.test.js diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js index 30f4ac3..44e2d9e 100644 --- a/parts/4/blogApp/src/utils.js +++ b/parts/4/blogApp/src/utils.js @@ -10,7 +10,13 @@ const average = (array) => { return array.length === 0 ? 0 : array.reduce(reducer, 0) / array.length; }; +const listHelper = (posts) => { + console.log("lol"); + return 1; +}; + module.exports = { reverse, average, + listHelper, }; diff --git a/parts/4/blogApp/tests/listHelper.test.js b/parts/4/blogApp/tests/listHelper.test.js new file mode 100644 index 0000000..b7b6881 --- /dev/null +++ b/parts/4/blogApp/tests/listHelper.test.js @@ -0,0 +1,10 @@ +const { test, describe } = require("node:test"); +const assert = require("node:assert"); +const { listHelper } = require("../src/utils"); + +test("dummy returns one", () => { + const blogs = []; + + const result = listHelper(blogs); + assert.strictEqual(result, 1); +}); diff --git a/parts/4/notes.md b/parts/4/notes.md index d51aac8..00c170f 100644 --- a/parts/4/notes.md +++ b/parts/4/notes.md @@ -1,3 +1,4 @@ Exercises: -- [X] 4.1 -- [X] 4.2 +* [X] 4.1 +* [X] 4.2 +* [X] 4.3 From 160402f14755bbb6ade4220601bfee84f38359da Mon Sep 17 00:00:00 2001 From: counterweight Date: Wed, 4 Jun 2025 23:47:43 +0200 Subject: [PATCH 07/10] completed 4.4 --- parts/4/blogApp/src/utils.js | 13 +++++++ parts/4/blogApp/tests/totalLikes.test.js | 43 ++++++++++++++++++++++++ parts/4/notes.md | 2 ++ 3 files changed, 58 insertions(+) create mode 100644 parts/4/blogApp/tests/totalLikes.test.js diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js index 44e2d9e..47920c8 100644 --- a/parts/4/blogApp/src/utils.js +++ b/parts/4/blogApp/src/utils.js @@ -15,8 +15,21 @@ const listHelper = (posts) => { return 1; }; +const totalLikes = (posts) => { + if (!posts) { + return 0; + } + + const likeCount = posts + .map((post) => post.likes) + .reduce((cum, value) => cum + value, 0); + + return likeCount; +}; + module.exports = { reverse, average, listHelper, + totalLikes, }; diff --git a/parts/4/blogApp/tests/totalLikes.test.js b/parts/4/blogApp/tests/totalLikes.test.js new file mode 100644 index 0000000..1bf1071 --- /dev/null +++ b/parts/4/blogApp/tests/totalLikes.test.js @@ -0,0 +1,43 @@ +const { test, describe } = require("node:test"); +const assert = require("node:assert"); +const { totalLikes } = require("../src/utils"); + +describe("total likes ", () => { + const posts = [ + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 5, + __v: 0, + }, + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 2, + __v: 0, + }, + ]; + + const emptyArray = []; + + const gibberish = "asdaSd123asd"; + + test("counts likes properly", () => { + assert.strictEqual(totalLikes(posts), 7); + }); + + test("works fine with empty array", () => { + assert.strictEqual(totalLikes(emptyArray), 0); + }); + + test("fails with gibberish input", () => { + const failedCall = () => { + totalLikes(gibberish); + }; + assert.throws(failedCall, Error); + }); +}); diff --git a/parts/4/notes.md b/parts/4/notes.md index 00c170f..0ee8e7f 100644 --- a/parts/4/notes.md +++ b/parts/4/notes.md @@ -2,3 +2,5 @@ Exercises: * [X] 4.1 * [X] 4.2 * [X] 4.3 +* [X] 4.4 +* From 0309a2cc95ee7d880a962b649e8bd0b831d8728f Mon Sep 17 00:00:00 2001 From: counterweight Date: Wed, 4 Jun 2025 23:59:57 +0200 Subject: [PATCH 08/10] completed 4.5 --- parts/4/blogApp/src/utils.js | 16 ++++++++ parts/4/blogApp/tests/favoritePost.test.js | 43 ++++++++++++++++++++++ parts/4/notes.md | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 parts/4/blogApp/tests/favoritePost.test.js diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js index 47920c8..e2f9ea5 100644 --- a/parts/4/blogApp/src/utils.js +++ b/parts/4/blogApp/src/utils.js @@ -27,9 +27,25 @@ const totalLikes = (posts) => { return likeCount; }; +const favoritePost = (posts) => { + if (!posts || posts.length === 0) { + return null; + } + + const highestLikes = posts.reduce( + (max, post) => (post.likes > max ? (max = post.likes) : max), + 0 + ); + + const favoritePost = posts.find((post) => post.likes == highestLikes); + + return favoritePost; +}; + module.exports = { reverse, average, listHelper, totalLikes, + favoritePost, }; diff --git a/parts/4/blogApp/tests/favoritePost.test.js b/parts/4/blogApp/tests/favoritePost.test.js new file mode 100644 index 0000000..d452e35 --- /dev/null +++ b/parts/4/blogApp/tests/favoritePost.test.js @@ -0,0 +1,43 @@ +const { test, describe } = require("node:test"); +const assert = require("node:assert"); +const { favoritePost } = require("../src/utils"); + +describe("favoritePost ", () => { + const posts = [ + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 5, + __v: 0, + }, + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 2, + __v: 0, + }, + ]; + + const emptyArray = []; + + const gibberish = "asdaSd123asd"; + + test("finds top properly", () => { + assert.strictEqual(favoritePost(posts), posts[0]); + }); + + test("works fine with empty array", () => { + assert.strictEqual(favoritePost(emptyArray), null); + }); + + test("fails with gibberish input", () => { + const failedCall = () => { + favoritePost(gibberish); + }; + assert.throws(failedCall, Error); + }); +}); diff --git a/parts/4/notes.md b/parts/4/notes.md index 0ee8e7f..8263903 100644 --- a/parts/4/notes.md +++ b/parts/4/notes.md @@ -3,4 +3,4 @@ Exercises: * [X] 4.2 * [X] 4.3 * [X] 4.4 -* +* [X] 4.5 From e51f318929322b1bfef4a75e6c26d10821d051b9 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Thu, 5 Jun 2025 15:44:55 +0200 Subject: [PATCH 09/10] exercise 4.6 completed --- parts/4/blogApp/src/utils.js | 20 +++++++++ parts/4/blogApp/tests/mostPosts.test.js | 54 +++++++++++++++++++++++++ parts/4/notes.md | 1 + 3 files changed, 75 insertions(+) create mode 100644 parts/4/blogApp/tests/mostPosts.test.js diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js index e2f9ea5..eadc74d 100644 --- a/parts/4/blogApp/src/utils.js +++ b/parts/4/blogApp/src/utils.js @@ -42,10 +42,30 @@ const favoritePost = (posts) => { return favoritePost; }; +const mostPosts = (posts) => { + if (!posts || posts.length === 0) { + return null; + } + + const countMap = posts.reduce((acc, post) => { + acc[post.author] = (acc[post.author] || 0) + 1; + return acc; + }, {}); + + // Use Object.entries to find the top author + const [author, postsCount] = Object.entries(countMap).reduce( + (max, entry) => (entry[1] > max[1] ? entry : max), + ['', 0] + ); + + return { author, posts: postsCount }; +}; + module.exports = { reverse, average, listHelper, totalLikes, favoritePost, + mostPosts, }; diff --git a/parts/4/blogApp/tests/mostPosts.test.js b/parts/4/blogApp/tests/mostPosts.test.js new file mode 100644 index 0000000..1b32452 --- /dev/null +++ b/parts/4/blogApp/tests/mostPosts.test.js @@ -0,0 +1,54 @@ +const { test, describe } = require("node:test"); +const assert = require("node:assert"); +const { mostPosts } = require("../src/utils"); + +describe("most posts ", () => { + const posts = [ + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 5, + __v: 0, + }, + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 2, + __v: 0, + }, + { + _id: "123", + title: "Lololo", + author: "John Doe", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 1, + __v: 0, + }, + ]; + + const emptyArray = []; + + const gibberish = "asdaSd123asd"; + + test("finds top author properly", () => { + assert.deepStrictEqual(mostPosts(posts), { + author: "Edsger W. Dijkstra", + posts: 2, + }); + }); + + test("works fine with empty array", () => { + assert.strictEqual(mostPosts(emptyArray), null); + }); + + test("fails with gibberish input", () => { + const failedCall = () => { + mostPosts(gibberish); + }; + assert.throws(failedCall, Error); + }); +}); diff --git a/parts/4/notes.md b/parts/4/notes.md index 8263903..ccd63b9 100644 --- a/parts/4/notes.md +++ b/parts/4/notes.md @@ -4,3 +4,4 @@ Exercises: * [X] 4.3 * [X] 4.4 * [X] 4.5 +* [X] 4.6 From b839aeba78fb51b0e4a542fc5dd76cb4ec718db7 Mon Sep 17 00:00:00 2001 From: Pablo Martin Date: Thu, 5 Jun 2025 15:48:19 +0200 Subject: [PATCH 10/10] completed exercise 4.7 --- parts/4/blogApp/src/utils.js | 20 ++++++++- parts/4/blogApp/tests/mostLikes.test.js | 54 +++++++++++++++++++++++++ parts/4/notes.md | 1 + 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 parts/4/blogApp/tests/mostLikes.test.js diff --git a/parts/4/blogApp/src/utils.js b/parts/4/blogApp/src/utils.js index eadc74d..9653c37 100644 --- a/parts/4/blogApp/src/utils.js +++ b/parts/4/blogApp/src/utils.js @@ -52,7 +52,6 @@ const mostPosts = (posts) => { return acc; }, {}); - // Use Object.entries to find the top author const [author, postsCount] = Object.entries(countMap).reduce( (max, entry) => (entry[1] > max[1] ? entry : max), ['', 0] @@ -61,6 +60,24 @@ const mostPosts = (posts) => { return { author, posts: postsCount }; }; +const mostLikes = (posts) => { + if (!posts || posts.length === 0) { + return null; + } + + const likesMap = posts.reduce((acc, post) => { + acc[post.author] = (acc[post.author] || 0) + post.likes; + return acc; + }, {}); + + const [author, likesCount] = Object.entries(likesMap).reduce( + (max, entry) => (entry[1] > max[1] ? entry : max), + ['', 0] + ); + + return { author, likes: likesCount }; + }; + module.exports = { reverse, average, @@ -68,4 +85,5 @@ module.exports = { totalLikes, favoritePost, mostPosts, + mostLikes }; diff --git a/parts/4/blogApp/tests/mostLikes.test.js b/parts/4/blogApp/tests/mostLikes.test.js new file mode 100644 index 0000000..2977375 --- /dev/null +++ b/parts/4/blogApp/tests/mostLikes.test.js @@ -0,0 +1,54 @@ +const { test, describe } = require("node:test"); +const assert = require("node:assert"); +const { mostLikes } = require("../src/utils"); + +describe("most likes ", () => { + const posts = [ + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 5, + __v: 0, + }, + { + _id: "5a422aa71b54a676234d17f8", + title: "Go To Statement Considered Harmful", + author: "Edsger W. Dijkstra", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 2, + __v: 0, + }, + { + _id: "123", + title: "Lololo", + author: "John Doe", + url: "https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf", + likes: 1, + __v: 0, + }, + ]; + + const emptyArray = []; + + const gibberish = "asdaSd123asd"; + + test("finds top author properly", () => { + assert.deepStrictEqual(mostLikes(posts), { + author: "Edsger W. Dijkstra", + likes: 7, + }); + }); + + test("works fine with empty array", () => { + assert.strictEqual(mostLikes(emptyArray), null); + }); + + test("fails with gibberish input", () => { + const failedCall = () => { + mostLikes(gibberish); + }; + assert.throws(failedCall, Error); + }); +}); diff --git a/parts/4/notes.md b/parts/4/notes.md index ccd63b9..e4593b6 100644 --- a/parts/4/notes.md +++ b/parts/4/notes.md @@ -5,3 +5,4 @@ Exercises: * [X] 4.4 * [X] 4.5 * [X] 4.6 +* [X] 4.7