From d238c35a1e21541fc8d92dd8724cd4ced29a97b3 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:17:01 +0200 Subject: [PATCH 01/27] up to control flow --- README.md | 32 +++---- fundamentals/Fundamentals/Exercises/Arrays.cs | 39 +++++++- .../Fundamentals/Exercises/ControlFlow.cs | 93 +++++++++++++++++-- .../Fundamentals/Exercises/Numbers.cs | 7 +- .../Fundamentals/Exercises/Strings.cs | 21 ++++- 5 files changed, 156 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 3a13b0d..be7996d 100644 --- a/README.md +++ b/README.md @@ -12,28 +12,28 @@ 2. You can follow the [official installation guide](https://learn.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=visualstudio#step-4---choose-workloads) making sure to select "ASP.NET and web development" and the `.NET desktop development` workloads. Make sure to also go to `Individual Components` -> `.NET 10 Runtime` (see screenshot) 3. Verify installation by launching Visual Studio on your Windows machine - ![.NET 10 Runtime](screenshots/net_10_runtime.png) ## 2. Language tour (reading ~25 min) -Please *read fully* the following guides, ~25 min +Please _read fully_ the following guides, ~25 min -- [ ] [C# Overview](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/overview) -> this explains file based or project based apps (we will use project based) -- [ ] [Program structure](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/) -> we will use project based apps with a `Program.cs` that uses **top level statements** instead of a `Main` method -- [ ] [Namespaces and using directives](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/namespaces) -- [ ] [Top level statements detail](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) -- [ ] [Type system overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/) -> C# is a typed language, so we need to learn about types and how to use them -- [ ] [Built-in types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/built-in-types) -> continuation from overview -- [ ] [Generics (lists)](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics) -- [ ] [Classes](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/classes) -- [ ] [OOP](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/) -- [ ] [OOP - objects](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/objects) -- [ ] [Exceptions and errors overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/) +- [x] [C# Overview](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/overview) -> this explains file based or project based apps (we will use project based) +- [x] [Program structure](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/) -> we will use project based apps with a `Program.cs` that uses **top level statements** instead of a `Main` method +- [x] [Namespaces and using directives](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/namespaces) +- [x] [Top level statements detail](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) +- [x] [Type system overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/) -> C# is a typed language, so we need to learn about types and how to use them +- [x] [Built-in types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/built-in-types) -> continuation from overview +- [x] [Generics (lists)](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics) +- [x] [Classes](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/classes) +- [x] [OOP](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/) +- [x] [OOP - objects](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/objects) +- [x] [Exceptions and errors overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/) Additional useful reading: -- [ ] [Differences for python developers](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/tips-for-python-developers) -> important bits are: syntax, use of tokens and `;` to separate code blocks (similar to JS), generics and nullable types -- [ ] [Differences for javascript developers](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/tips-for-javascript-developers) -> syntax, async/await (will be useful towards end of our lessons) + +- [x] [Differences for python developers](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/tips-for-python-developers) -> important bits are: syntax, use of tokens and `;` to separate code blocks (similar to JS), generics and nullable types +- [x] [Differences for javascript developers](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/tips-for-javascript-developers) -> syntax, async/await (will be useful towards end of our lessons) You should be familiar, by the end of the reading, with the following key concepts: @@ -115,7 +115,7 @@ Once the core themes' exercises are green in Test Explorer, tackle the advanced - `StringsAdvanced` — `StringBuilder` + a small CSV-row parser. - `ArraysAdvanced` — multi-dimensional (`int[,]`) vs jagged (`int[][]`) arrays + a matrix transpose and a duplicate-detection exercise. -- `ControlFlowAdvanced` — `switch` *expression* (C# 8+) and relational patterns (`>= 70 => "A"`) for collapsing else-if chains into one tidy block. +- `ControlFlowAdvanced` — `switch` _expression_ (C# 8+) and relational patterns (`>= 70 => "A"`) for collapsing else-if chains into one tidy block. ## 4. Bank project (capstone) diff --git a/fundamentals/Fundamentals/Exercises/Arrays.cs b/fundamentals/Fundamentals/Exercises/Arrays.cs index e2a7487..6c64336 100644 --- a/fundamentals/Fundamentals/Exercises/Arrays.cs +++ b/fundamentals/Fundamentals/Exercises/Arrays.cs @@ -11,7 +11,13 @@ public static class Arrays // Hint: Lesson F showed two ways to iterate — pick one and accumulate a total. public static int SumOfArray(int[] numbers) { - throw new NotImplementedException("TODO: iterate and accumulate a total"); + int sum = 0; + foreach (int number in numbers) + { + sum += number; + } + + return sum; } // EXERCISE 2: FindLargest @@ -23,7 +29,16 @@ public static int SumOfArray(int[] numbers) // see something bigger. public static int FindLargest(int[] numbers) { - throw new NotImplementedException("TODO: track the largest seen so far"); + int largest = int.MinValue; + foreach (int number in numbers) + { + if (number > largest) + { + largest = number; + } + } + + return largest; } // EXERCISE 3: CountEvens @@ -33,7 +48,16 @@ public static int FindLargest(int[] numbers) // Hint: a number is even when `n % 2 == 0`. Increment a counter each time. public static int CountEvens(int[] numbers) { - throw new NotImplementedException("TODO: count values where n % 2 == 0"); + int count = 0; + foreach (int number in numbers) + { + if (number % 2 == 0) + { + count++; + } + } + + return count; } // EXERCISE 4: ReverseArray @@ -45,6 +69,13 @@ public static int CountEvens(int[] numbers) // source index and the destination index move in opposite directions. public static int[] ReverseArray(int[] numbers) { - throw new NotImplementedException("TODO: build a new array with indices walking in opposite directions"); + int[] newArr = new int[numbers.Length]; + int destIndex = numbers.Length - 1; + foreach (int number in numbers) + { + newArr[destIndex] = number; + destIndex--; + } + return newArr; } } diff --git a/fundamentals/Fundamentals/Exercises/ControlFlow.cs b/fundamentals/Fundamentals/Exercises/ControlFlow.cs index a4ba1cf..cb8f496 100644 --- a/fundamentals/Fundamentals/Exercises/ControlFlow.cs +++ b/fundamentals/Fundamentals/Exercises/ControlFlow.cs @@ -10,7 +10,18 @@ public static class ControlFlow // Hint: if / else if / else chain. See Lesson A. public static string Sign(int n) { - throw new NotImplementedException("TODO: compare n against 0 with if / else if / else"); + if (n > 0) + { + return "positive"; + } + else if (n < 0) + { + return "negative"; + } + else + { + return "zero"; + } } // EXERCISE 2: IsLeapYear @@ -22,7 +33,12 @@ public static string Sign(int n) // Hint: combine conditions with && and ||, or nest ifs. Either approach works. public static bool IsLeapYear(int year) { - throw new NotImplementedException("TODO: apply the divisible-by-4 / 100 / 400 rules"); + if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) + { + return true; + } + + return false; } // EXERCISE 3: DayName @@ -31,7 +47,25 @@ public static bool IsLeapYear(int year) // Hint: switch STATEMENT with 7 cases + default. See Lesson D. public static string DayName(int day) { - throw new NotImplementedException("TODO: switch on day, return the 3-letter name"); + switch (day) + { + case 1: + return "Mon"; + case 2: + return "Tue"; + case 3: + return "Wed"; + case 4: + return "Thu"; + case 5: + return "Fri"; + case 6: + return "Sat"; + case 7: + return "Sun"; + default: + return "?"; + } } // EXERCISE 4: IsVowel @@ -40,7 +74,17 @@ public static string DayName(int day) // Hint: switch STATEMENT with stacked case labels. Lowercase first via char.ToLower(c). public static bool IsVowel(char c) { - throw new NotImplementedException("TODO: switch on the lowercased char, stack the vowel cases"); + switch (char.ToLower(c)) + { + case 'a': + case 'e': + case 'i': + case 'o': + case 'u': + return true; + default: + return false; + } } // EXERCISE 5: CountPositives @@ -49,7 +93,16 @@ public static bool IsVowel(char c) // Hint: foreach + if. See Lesson H. public static int CountPositives(int[] nums) { - throw new NotImplementedException("TODO: foreach over nums, increment a counter when > 0"); + int count = 0; + foreach (int number in nums) + { + if (number > 0) + { + count++; + } + } + + return count; } // EXERCISE 6: FirstIndexOf @@ -59,7 +112,15 @@ public static int CountPositives(int[] nums) // Return `i` as soon as you find a match. public static int FirstIndexOf(int[] nums, int target) { - throw new NotImplementedException("TODO: for loop with index; return i when nums[i] == target; else -1"); + for (int i = 0; i < nums.Length; i++) + { + if (nums[i] == target) + { + return i; + } + } + + return -1; } // EXERCISE 7: SumUntilNegative @@ -69,7 +130,18 @@ public static int FirstIndexOf(int[] nums, int target) // Hint: foreach + if + break. See Lesson F for break. public static int SumUntilNegative(int[] nums) { - throw new NotImplementedException("TODO: foreach; if n < 0 break; otherwise add to total"); + int sum = 0; + foreach (int number in nums) + { + if (number < 0) + { + break; + } + + sum += number; + } + + return sum; } // EXERCISE 8: ReverseArray @@ -80,6 +152,11 @@ public static int SumUntilNegative(int[] nums) // nums[nums.Length - 1 - i] into result[i]. public static int[] ReverseArray(int[] nums) { - throw new NotImplementedException("TODO: allocate a new int[], copy nums in reverse order"); + int[] result = new int[nums.Length]; + for (int i = 0; i < nums.Length; i++) + { + result[i] = nums[nums.Length - 1 - i]; + } + return result; } } diff --git a/fundamentals/Fundamentals/Exercises/Numbers.cs b/fundamentals/Fundamentals/Exercises/Numbers.cs index 344b946..5fbece6 100644 --- a/fundamentals/Fundamentals/Exercises/Numbers.cs +++ b/fundamentals/Fundamentals/Exercises/Numbers.cs @@ -8,14 +8,14 @@ public static class Numbers // Return the sum of two integers. public static int Add(int a, int b) { - throw new NotImplementedException("TODO: return a + b"); + return a + b; } // EXERCISE 2: // Return the area of a rectangle (width × height). public static double RectangleArea(double width, double height) { - throw new NotImplementedException("TODO: multiply width by height"); + return width * height; } // EXERCISE 3: @@ -25,6 +25,7 @@ public static double RectangleArea(double width, double height) // not 1.8. Cast appropriately so you get the right answer. public static double CelsiusToFahrenheit(double celsius) { - throw new NotImplementedException("TODO: apply the formula, mind the division"); + double fahrenheit = celsius * 9 / 5 + 32; + return fahrenheit; } } diff --git a/fundamentals/Fundamentals/Exercises/Strings.cs b/fundamentals/Fundamentals/Exercises/Strings.cs index 857d38e..de4659e 100644 --- a/fundamentals/Fundamentals/Exercises/Strings.cs +++ b/fundamentals/Fundamentals/Exercises/Strings.cs @@ -9,7 +9,7 @@ public static class Strings // Example: Shout("hello") → "HELLO!!!" public static string Shout(string message) { - throw new NotImplementedException("TODO: uppercase the message and append !!!"); + return message.ToUpper() + "!!!"; } // EXERCISE 2: CountVowels @@ -19,7 +19,18 @@ public static string Shout(string message) // of vowels. Lowercase the char first with char.ToLower(c). public static int CountVowels(string text) { - throw new NotImplementedException("TODO: loop over each char, count vowels"); + HashSet vowels = [ 'a', 'e', 'i', 'o', 'u' ]; + + int count = 0; + foreach (char character in text) + { + if (vowels.Contains(char.ToLower(character))) + { + count++; + } + } + + return count; } // EXERCISE 3: IsPalindrome @@ -29,7 +40,7 @@ public static int CountVowels(string text) // new string(word.Reverse().ToArray()) — Reverse comes from LINQ (already using'd). public static bool IsPalindrome(string word) { - throw new NotImplementedException("TODO: compare word to its reverse, case-insensitive"); + return string.Equals(word, new string(word.Reverse().ToArray()), StringComparison.OrdinalIgnoreCase); } // EXERCISE 4: FormatPrice @@ -40,7 +51,7 @@ public static bool IsPalindrome(string word) // the output is the same regardless of system locale.) public static string FormatPrice(decimal amount) { - throw new NotImplementedException("TODO: prepend £ and format to 2 decimals"); + return $"£{amount:F2}"; } // EXERCISE 5: SafeParseInt @@ -49,6 +60,6 @@ public static string FormatPrice(decimal amount) // Hint: use int.TryParse — don't let a FormatException escape. public static int SafeParseInt(string input) { - throw new NotImplementedException("TODO: use int.TryParse and return -1 on failure"); + return int.TryParse(input, out int parsed) ? parsed : -1; } } From 833a3dbbebec7b453f6a648deab20097791ec7f6 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:47:39 +0200 Subject: [PATCH 02/27] fundementals (except advanced) --- .../Fundamentals/Exercises/Classes.cs | 10 +-- fundamentals/Fundamentals/Exercises/Enums.cs | 20 ++++- .../Fundamentals/Exercises/Exceptions.cs | 23 +++++- fundamentals/Fundamentals/Exercises/Lists.cs | 73 +++++++++++++++++-- .../Fundamentals/Exercises/Structs.cs | 9 ++- 5 files changed, 113 insertions(+), 22 deletions(-) diff --git a/fundamentals/Fundamentals/Exercises/Classes.cs b/fundamentals/Fundamentals/Exercises/Classes.cs index d9538fe..462da3d 100644 --- a/fundamentals/Fundamentals/Exercises/Classes.cs +++ b/fundamentals/Fundamentals/Exercises/Classes.cs @@ -23,7 +23,7 @@ public class ShoppingCart // exactly this pattern for its Students list. public ShoppingCart() { - throw new NotImplementedException("TODO: initialise Items to an empty list"); + Items = []; } // EXERCISE 2: Add @@ -31,7 +31,7 @@ public ShoppingCart() // Example: cart.Add("apple"); cart.Count() → 1 public void Add(string item) { - throw new NotImplementedException("TODO: call Items.Add(item)"); + Items.Add(item); } // EXERCISE 3: Count @@ -39,7 +39,7 @@ public void Add(string item) // Example: empty cart → 0; after two Add calls → 2 public int Count() { - throw new NotImplementedException("TODO: return Items.Count"); + return Items.Count; } // EXERCISE 4: Contains @@ -48,7 +48,7 @@ public int Count() // Hint: List has a built-in .Contains method — you can delegate to it. public bool Contains(string item) { - throw new NotImplementedException("TODO: return Items.Contains(item)"); + return Items.Contains(item); } // EXERCISE 5: Remove @@ -59,6 +59,6 @@ public bool Contains(string item) // Hint: List.Remove(item) already returns the bool you need. public bool Remove(string item) { - throw new NotImplementedException("TODO: return Items.Remove(item)"); + return Items.Remove(item); } } diff --git a/fundamentals/Fundamentals/Exercises/Enums.cs b/fundamentals/Fundamentals/Exercises/Enums.cs index 0717ec6..36fdb52 100644 --- a/fundamentals/Fundamentals/Exercises/Enums.cs +++ b/fundamentals/Fundamentals/Exercises/Enums.cs @@ -1,3 +1,5 @@ +using Fundamentals.Lessons; + namespace Fundamentals.Exercises; // Theme: Enums — exercises for you to implement. @@ -27,7 +29,19 @@ public static class Enums // Hint: Lesson C in Enums.cs shows the exact shape. public static string Prompt(VendingMachineState state) { - throw new NotImplementedException("TODO: switch on state, return the matching prompt string"); + switch (state) + { + case VendingMachineState.Idle: + return "Insert a coin"; + case VendingMachineState.CoinInserted: + return "Select a product"; + case VendingMachineState.Dispensing: + return "Please wait..."; + case VendingMachineState.OutOfStock: + return "Sold out"; + default: + throw new ArgumentOutOfRangeException(nameof(state)); + } } // EXERCISE 2: CanAcceptCoin @@ -39,7 +53,7 @@ public static string Prompt(VendingMachineState state) // Hint: enum values compare with `==` — no switch needed for this one. public static bool CanAcceptCoin(VendingMachineState state) { - throw new NotImplementedException("TODO: true when state equals VendingMachineState.Idle"); + return state == VendingMachineState.Idle; } // EXERCISE 3: ParseState @@ -53,6 +67,6 @@ public static bool CanAcceptCoin(VendingMachineState state) // Hint: Enum.TryParse(text, ignoreCase: true, out state) does exactly this. public static bool ParseState(string text, out VendingMachineState state) { - throw new NotImplementedException("TODO: delegate to Enum.TryParse with ignoreCase: true"); + return Enum.TryParse(text, ignoreCase: true, out state); } } diff --git a/fundamentals/Fundamentals/Exercises/Exceptions.cs b/fundamentals/Fundamentals/Exercises/Exceptions.cs index ac7d475..435348f 100644 --- a/fundamentals/Fundamentals/Exercises/Exceptions.cs +++ b/fundamentals/Fundamentals/Exercises/Exceptions.cs @@ -16,7 +16,12 @@ public static class Exceptions // Use `nameof(n)` for the parameter name. public static int RequirePositive(int n) { - throw new NotImplementedException("TODO: throw ArgumentException when n <= 0, otherwise return n"); + if (n <= 0) + { + throw new ArgumentException("n must be positive", nameof(n)); + } + + return n; } // EXERCISE 2: Withdraw @@ -30,7 +35,12 @@ public static int RequirePositive(int n) // isn't in a state where this operation makes sense". public static int Withdraw(int balance, int amount) { - throw new NotImplementedException("TODO: throw InvalidOperationException when amount > balance, else return balance - amount"); + if (amount > balance) + { + throw new InvalidOperationException("insufficient funds"); + } + + return balance - amount; } // EXERCISE 3: SafeWithdraw @@ -43,6 +53,13 @@ public static int Withdraw(int balance, int amount) // Do NOT catch every exception — only the specific type you expect. public static int SafeWithdraw(int balance, int amount) { - throw new NotImplementedException("TODO: try Withdraw; on InvalidOperationException return balance unchanged"); + try + { + return Withdraw(balance, amount); + } + catch (InvalidOperationException ex) + { + return balance; + } } } diff --git a/fundamentals/Fundamentals/Exercises/Lists.cs b/fundamentals/Fundamentals/Exercises/Lists.cs index 9795219..71a6203 100644 --- a/fundamentals/Fundamentals/Exercises/Lists.cs +++ b/fundamentals/Fundamentals/Exercises/Lists.cs @@ -1,3 +1,5 @@ +using Fundamentals.Lessons; + namespace Fundamentals.Exercises; // Theme: Lists — exercises for you to implement. @@ -11,7 +13,13 @@ public static class Lists // Hint: Lesson F showed two ways to iterate a list — pick one and accumulate a total. public static int SumOfList(List numbers) { - throw new NotImplementedException("TODO: iterate and accumulate a total"); + int sum = 0; + foreach (int number in numbers) + { + sum += number; + } + + return sum; } // EXERCISE 2: FindLargest @@ -22,7 +30,16 @@ public static int SumOfList(List numbers) // and update your "largest so far" whenever you see something bigger. public static int FindLargest(List numbers) { - throw new NotImplementedException("TODO: track the largest seen so far"); + int largest = int.MinValue; + foreach (int number in numbers) + { + if (number > largest) + { + largest = number; + } + } + + return largest; } // EXERCISE 3: CountEvens @@ -32,7 +49,16 @@ public static int FindLargest(List numbers) // Hint: a number is even when `n % 2 == 0`. Increment a counter each time. public static int CountEvens(List numbers) { - throw new NotImplementedException("TODO: count values where n % 2 == 0"); + int count = 0; + foreach (int number in numbers) + { + if (number % 2 == 0) + { + count++; + } + } + + return count; } // EXERCISE 4: CountPositives @@ -41,7 +67,16 @@ public static int CountEvens(List numbers) // Hint: foreach + if. See Lesson F. public static int CountPositives(List numbers) { - throw new NotImplementedException("TODO: foreach over numbers, increment a counter when > 0"); + int count = 0; + foreach (int number in numbers) + { + if (number > 0) + { + count++; + } + } + + return count; } // EXERCISE 5: FirstIndexOf @@ -53,7 +88,15 @@ public static int CountPositives(List numbers) // Return `i` as soon as you find a match; return -1 after the loop. public static int FirstIndexOf(List numbers, int target) { - throw new NotImplementedException("TODO: for loop with index; return i when numbers[i] == target; else -1"); + for (int i = 0; i < numbers.Count; i++) + { + if (numbers[i] == target) + { + return i; + } + } + + return -1; } // EXERCISE 6: SumUntilNegative @@ -64,7 +107,18 @@ public static int FirstIndexOf(List numbers, int target) // Hint: foreach + if + break. public static int SumUntilNegative(List numbers) { - throw new NotImplementedException("TODO: foreach; if n < 0 break; otherwise add to total"); + int sum = 0; + foreach (int number in numbers) + { + if (number < 0) + { + break; + } + + sum += number; + } + + return sum; } // EXERCISE 7: ReverseList @@ -76,6 +130,11 @@ public static int SumUntilNegative(List numbers) // input from the last index down to 0, calling Add on the new list. public static List ReverseList(List numbers) { - throw new NotImplementedException("TODO: new List(); walk input last-to-first; Add each element"); + List newList = []; + for (int i = numbers.Count - 1; i >= 0; i--) + { + newList.Add(numbers[i]); + } + return newList; } } diff --git a/fundamentals/Fundamentals/Exercises/Structs.cs b/fundamentals/Fundamentals/Exercises/Structs.cs index 2a50bd0..b2e3a57 100644 --- a/fundamentals/Fundamentals/Exercises/Structs.cs +++ b/fundamentals/Fundamentals/Exercises/Structs.cs @@ -18,7 +18,8 @@ public struct Point // Hint: Lesson C in Structs.cs shows the pattern exactly. public Point(double x, double y) { - throw new NotImplementedException("TODO: assign x and y to X and Y"); + X = x; + Y = y; } // EXERCISE 2: IsOrigin @@ -27,7 +28,7 @@ public Point(double x, double y) // Example: new Point(3, 4).IsOrigin() → false public bool IsOrigin() { - throw new NotImplementedException("TODO: true when both X and Y are 0"); + return X == 0 && Y == 0; } // EXERCISE 3: DistanceTo @@ -37,7 +38,7 @@ public bool IsOrigin() // Example: new Point(0, 0).DistanceTo(new Point(3, 4)) → 5 public double DistanceTo(Point other) { - throw new NotImplementedException("TODO: Math.Sqrt of the sum of squared differences"); + return Math.Sqrt(Math.Pow((X - other.X), 2) + Math.Pow((Y - other.Y), 2)); } // EXERCISE 4: Translate @@ -46,6 +47,6 @@ public double DistanceTo(Point other) // Example: new Point(1, 2).Translate(10, 20) → new Point(11, 22) public Point Translate(double dx, double dy) { - throw new NotImplementedException("TODO: return new Point(X + dx, Y + dy)"); + return new Point(X + dx, Y + dy); } } From 27ebad7f7e96a4860b7c38a6eaeccf089dd8e260 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:40:46 +0200 Subject: [PATCH 03/27] advanced --- .../Fundamentals/Exercises/ArraysAdvanced.cs | 27 +++++++++++++++++-- .../Exercises/ControlFlowAdvanced.cs | 16 +++++++++-- .../Fundamentals/Exercises/StringsAdvanced.cs | 21 ++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/fundamentals/Fundamentals/Exercises/ArraysAdvanced.cs b/fundamentals/Fundamentals/Exercises/ArraysAdvanced.cs index 678c9f7..4cea084 100644 --- a/fundamentals/Fundamentals/Exercises/ArraysAdvanced.cs +++ b/fundamentals/Fundamentals/Exercises/ArraysAdvanced.cs @@ -25,7 +25,19 @@ public static class ArraysAdvanced // result[c, r] = matrix[r, c]. public static int[,] Transpose(int[,] matrix) { - throw new NotImplementedException("TODO: new int[cols, rows], copy with indices swapped"); + int rowCount = matrix.GetLength(0); + int colCount = matrix.GetLength(1); + int[,] result = new int[colCount, rowCount]; + + for (int r = 0; r < rowCount; r++) + { + for (int c = 0; c < colCount; c++) + { + result[c, r] = matrix[r, c]; + } + } + + return result; } // ADVANCED EXERCISE 2: HasDuplicates @@ -42,6 +54,17 @@ public static class ArraysAdvanced // Only return false once every pair has been checked. public static bool HasDuplicates(int[] numbers) { - throw new NotImplementedException("TODO: nested loops, compare each pair of indices"); + for (int i = 0; i < numbers.Length; i++) + { + for (int j = i + 1; j < numbers.Length; j++) + { + if (numbers[i] == numbers[j]) + { + return true; + } + } + } + + return false; } } diff --git a/fundamentals/Fundamentals/Exercises/ControlFlowAdvanced.cs b/fundamentals/Fundamentals/Exercises/ControlFlowAdvanced.cs index 585f005..6527d1c 100644 --- a/fundamentals/Fundamentals/Exercises/ControlFlowAdvanced.cs +++ b/fundamentals/Fundamentals/Exercises/ControlFlowAdvanced.cs @@ -11,7 +11,13 @@ public static class ControlFlowAdvanced // Hint: see Lesson J. Arms are tested top-to-bottom — put `>= 70` first. public static string GradeFromMark(int mark) { - throw new NotImplementedException("TODO: return mark switch { >= 70 => \"A\", ... }"); + return mark switch + { + >= 70 => "A", + >= 60 => "B", + >= 50 => "C", + _ => "F", + }; } // EXERCISE 2: TrafficLightAction @@ -27,6 +33,12 @@ public static string GradeFromMark(int mark) // "amber" or "yellow" => "prepare", public static string TrafficLightAction(string colour) { - throw new NotImplementedException("TODO: return colour switch { \"red\" => \"stop\", ... }"); + return colour switch + { + "red" => "stop", + "amber" or "yellow" => "prepare", + "green" => "go", + _ => "?" + }; } } diff --git a/fundamentals/Fundamentals/Exercises/StringsAdvanced.cs b/fundamentals/Fundamentals/Exercises/StringsAdvanced.cs index 0799231..ee33899 100644 --- a/fundamentals/Fundamentals/Exercises/StringsAdvanced.cs +++ b/fundamentals/Fundamentals/Exercises/StringsAdvanced.cs @@ -1,5 +1,7 @@ namespace Fundamentals.Exercises; +using System.Text; + // Theme: Strings (advanced) — exercises for you to implement. // The teaching material (Lesson G: StringBuilder) is in Lessons/StringsAdvanced.cs. // Tackle this only after finishing the core Strings exercises. @@ -33,6 +35,23 @@ public static class StringsAdvanced // You don't need to handle escaped quotes ("" inside a quoted field). public static string[] ParseCsvRow(string row) { - throw new NotImplementedException("TODO: walk the chars, track insideQuotes, build each field with StringBuilder"); + List fields = []; + bool insideQuotes = false; + var currentField = new StringBuilder(); + foreach (char c in row) + { + if (c == '"') + insideQuotes = !insideQuotes; + else if (c == ',' && !insideQuotes) + { + fields.Add(currentField.ToString()); + currentField.Clear(); + } + else + currentField.Append(c); + } + + fields.Add(currentField.ToString()); + return fields.ToArray(); } } From 75bc7f71839d9cf4fbe95923f6d118941c8f3410 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:31:50 +0200 Subject: [PATCH 04/27] notes --- csharp-basics-04-23.md | 310 +++++++++++++++++++++++++++++++++++++++++ csharp.md | 75 ++++++++++ 2 files changed, 385 insertions(+) create mode 100644 csharp-basics-04-23.md create mode 100644 csharp.md diff --git a/csharp-basics-04-23.md b/csharp-basics-04-23.md new file mode 100644 index 0000000..f890c60 --- /dev/null +++ b/csharp-basics-04-23.md @@ -0,0 +1,310 @@ +# C# Basics - Day 1 + +[GitHub Repo](https://github.com/boolean-uk/csharp-4day-course) + +## how to create a fresh console program in Visual Studio (not file based, but solution based) + +1. Open Visual Studio +2. Create a new 'Console App' +3. Alternatively, use `dotnet new console` + +**File-based:** Single .cs file, no project file +**Project-based:** Multiple .cs files, with a .csproj file to manage them + +```cs +using System; + +class Program +{ + static void Main() + { + Console.WriteLine("Hello, World"); + } +} +``` + +`dotnet run hello-world.cs` +`dotnet build hello-world.cs` + +## what Program.cs is and why we can avoid a Main function (top level statements) + +One file in each project can contain top-level statements instead of a Main function, with the entrypoint being the first line of code. Same as using a Main function, but less boilerplate. Program.cs is the default entrypoint file. (can only have one entrypoint) + +## defining variables of different types to store strings, integers, booleans, arrays + +[Cheatsheet](https://github.com/milanm/csharp-cheatsheet) + +### Comments + +```cs +// This is a single-line comment + +/* This is a + multi-line comment */ +``` + +### Strings + +```cs +string greeting = "Hello"; +string name = "World"; +string message = greeting + " " + name; // "Hello World" +string interpolated = $"{greeting} {name}!"; // "Hello World!" + +// Common string methods +bool contains = text.Contains("World"); // true +string upper = text.ToUpper(); // "HELLO, WORLD!" +string lower = text.ToLower(); // "hello, world!" +string replaced = text.Replace("Hello", "Hi"); // "Hi, World!" +string trimmed = " text ".Trim(); // "text" +string[] split = text.Split(','); // ["Hello", " World!"] +int length = text.Length; // 13 +``` + +### Basic types + +```cs +// Integers +byte byteValue = 255; // 8-bit unsigned integer (0 to 255) +sbyte sbyteValue = -128; // 8-bit signed integer (-128 to 127) +short shortValue = -32768; // 16-bit signed integer (-32,768 to 32,767) +ushort ushortValue = 65535; // 16-bit unsigned integer (0 to 65,535) +int intValue = -2147483648; // 32-bit signed integer (-2,147,483,648 to 2,147,483,647) +uint uintValue = 4294967295; // 32-bit unsigned integer (0 to 4,294,967,295) +long longValue = -9223372036854775808; // 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807) +ulong ulongValue = 18446744073709551615; // 64-bit unsigned integer (0 to 18,446,744,073,709,551,615) + +// Floats +float floatValue = 3.14f; // 32-bit floating-point (7 significant digits precision) +double doubleValue = 3.14159265359; // 64-bit floating-point (15-16 significant digits precision) +decimal decimalValue = 3.14159265359m; // 128-bit high-precision decimal (28-29 significant digits) + +// Booleans +bool isTrue = true; +bool isFalse = false; + +// Chars +char letter = 'A'; +char unicodeChar = '\u0041'; // Unicode character for 'A' +char escapeChar = '\n'; // Newline + +// DateTime and TimeSpan +DateTime now = DateTime.Now; +DateTime utcNow = DateTime.UtcNow; +DateOnly today = DateOnly.FromDateTime(DateTime.Today); // Date without time (C# 10+) +TimeOnly noon = new TimeOnly(12, 0, 0); // Time without date (C# 10+) +DateTime specific = new DateTime(2023, 1, 1); +TimeSpan oneHour = TimeSpan.FromHours(1); +TimeSpan duration = TimeSpan.FromMinutes(90); + +// Nullable types (can be null) +int? nullableInt = null; +bool? nullableBool = null; + +// Vars (inferred) +var inferredInt = 42; // Compiler infers int +var inferredString = "Hello"; // Compiler infers string +var inferredList = new List(); // Compiler infers List + +// Compile-time constants (must be primitive types or string) +const double Pi = 3.14159; +const string AppName = "MyApp"; + +// Runtime constants +readonly DateTime StartTime = DateTime.Now; +public static readonly HttpClient SharedClient = new HttpClient(); // initialized only once at runtime +``` + +### Arrays (fixed size) + +```cs +int[] numbers = new int[5]; // Array of 5 integers with default values (0) +int[] initialized = new int[] { 1, 2, 3, 4, 5 }; // Initialized array +int[] shorthand = { 1, 2, 3, 4, 5 }; // Shorthand initialization + +// Accessing elements +int firstNumber = numbers[0]; // First element +numbers[0] = 10; // Assign value to first element + +// Array properties and methods +int length = numbers.Length; // Number of elements +Array.Sort(numbers); // Sort array in-place +Array.Reverse(numbers); // Reverse array in-place +int index = Array.IndexOf(names, "Bob"); // Find index of element +bool exists = Array.Exists(numbers, n => n > 10); // Check if condition exists +``` + +### Lists (dynamic size) + +```cs +using System.Collections.Generic; + +List names = new List(); // Empty list +List numbers = new List { 1, 2, 3 }; // Initialized list + +// Add elements +names.Add("Alice"); // Add single element +names.AddRange(new[] { "Bob", "Charlie" }); // Add multiple elements + +// Access elements +string first = names[0]; // Access by index +names[0] = "Alicia"; // Modify by index + +// Remove elements +names.Remove("Bob"); // Remove specific element +names.RemoveAt(0); // Remove element at index +names.RemoveAll(x => x.StartsWith("C")); // Remove all that match condition +names.Clear(); // Remove all elements + +// Search and query +bool contains = numbers.Contains(2); // Check if contains value +int index = numbers.IndexOf(3); // Find index of element +List filtered = numbers.FindAll(n => n > 1); // Find all matching elements +int found = numbers.Find(n => n > 2); // Find first matching element + +// Other operations +int count = numbers.Count; // Number of elements +numbers.Sort(); // Sort list in-place +numbers.Reverse(); // Reverse list in-place +numbers.ForEach(n => Console.WriteLine(n)); // Perform action on each element +``` + +### Dictionary (key-value pairs, similar to Python dict or JavaScript object) + +```cs +using System.Collections.Generic; + +Dictionary ages = new Dictionary(); +Dictionary capitals = new Dictionary +{ + { "USA", "Washington D.C." }, + { "UK", "London" }, + ["France"] = "Paris" // Alternative initialization syntax +}; + +// Add entries +ages.Add("Alice", 30); +ages["Bob"] = 25; // Add or update using indexer + +// Access values +int aliceAge = ages["Alice"]; // Access by key (throws if not found) +bool success = ages.TryGetValue("Charlie", out int charlieAge); // Safe access + +// Check existence +bool containsKey = ages.ContainsKey("Alice"); +bool containsValue = ages.ContainsValue(25); + +// Remove entries +bool removed = ages.Remove("Bob"); + +// Iterate through dictionary +foreach (KeyValuePair pair in ages) +{ + Console.WriteLine($"{pair.Key}: {pair.Value}"); +} + +// Or using deconstruction (C# 7.0+) +foreach (var (name, age) in ages) +{ + Console.WriteLine($"{name}: {age}"); +} +``` + +## Methods and Functions + +- must live inside Classes - this is a core difference with Python, Javascript where you can have functions defined freely +- it is possible to define functions inside other functions (local functions) + +```cs +// Instance method +public int Add(int a, int b) +{ + return a + b; +} + +// Static method +public static double CalculateArea(double radius) +{ + return Math.PI * radius * radius; +} + +// Void method (no return value) +public void PrintMessage(string message) +{ + Console.WriteLine(message); +} + +// Optional parameters +public void Greet(string name, string greeting = "Hello") +{ + Console.WriteLine($"{greeting}, {name}!"); +} + +// Named arguments +Greet(greeting: "Hi", name: "Alice"); +``` + +## Classes and Objects + +```cs +// Basic class definition +public class Person +{ + // Fields + private string name; + private int age; + + // Properties + public string Name + { + get { return name; } + set { name = value; } + } + + // Auto-implemented property + public int Age { get; set; } + + // Read-only property + public bool IsAdult => Age >= 18; + + // Constructors + public Person() + { + // Default constructor + } + + public Person(string name, int age) + { + Name = name; + Age = age; + } + + // Methods + public void Introduce() + { + Console.WriteLine($"Hello, my name is {Name} and I'm {Age} years old."); + } + + public string GetDescription() => $"{Name}, {Age} years old"; + + // Static members + public static int MinimumAge { get; } = 0; + + public static bool IsValidAge(int age) + { + return age >= MinimumAge; + } +} + +// Usage +Person person = new Person(); +person.Name = "Alice"; +person.Age = 30; +person.Introduce(); + +Person bob = new Person("Bob", 25); +string description = bob.GetDescription(); +bool isAdult = bob.IsAdult; + +bool isValid = Person.IsValidAge(20); +``` diff --git a/csharp.md b/csharp.md new file mode 100644 index 0000000..cea62f0 --- /dev/null +++ b/csharp.md @@ -0,0 +1,75 @@ +**Int** + +- 7 / 2 = 3 (int) + +**String** + +- '' = char, "" = string +- prefix string with @ to avoid escaping backslash, etc (@"C:\\Users") (except for ""hello"") +- interpolation with $ ($"Hello {name}") +- string methods like ToUpper() does not modify original string (creates new string) +- .Length .Split .Join .Contains etc +- string.Equals() string.IsNullOrWhiteSpace() +- int.Parse() int.TryParse() + +String formatting: + +- :C currency (culture-aware: £ in en-GB, $ in en-US) +- :N2 number with 2 decimal places and thousands separators +- :F2 fixed-point, 2 decimal places, no thousands separator +- :D5 integer zero-padded to 5 digits +- :P1 percentage with 1 decimal place + +**Arrays** + +- arrays are fixed while lists are dynamic (size) +- defaults: + + int → 0 + + double → 0.0 + + bool → false + + string → null (reference types default to null) + +- use for loop when you need index otherwise foreach + + or: foreach (string name in names.Select((value, index) => new { value, index })) + +- Array.Sort() Array.Reverse() Array.IndexOf() + +**Switch** + +- have to use break/return/throw +- possible to stack case labels + +**List** + +- .Count + +**OOP** + +- struct vs class: + + struct creates deep copy when assigned or passed to function (fix by returning new copy in function) + + class is single shared instance (default to classes) + +- protected: not accessible outside (object) the class/inherited classes +- always try to keep logic to private variables and keep public variables small, and only expose what you need (so we don't have to change every implementation when refactoring public variable) +- abstract class: cannot create object of class +- virtual: +- use structs for small data, e.g. coordinates, money, date ranges + use classes when data gets more complex than that +- polymorphism: overriding parent methods + +**Enums** + +- increasing int under the hood +- ToString() returns name not int + +**Exceptions** + +- try/catch +- use specific built-in csharp error (Exception for all, avoid) From 11bbeafc34635666e895bb84190103a0d6f53416 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:03:16 +0200 Subject: [PATCH 05/27] forgot to push advanced notes --- csharp.md | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/csharp.md b/csharp.md index cea62f0..0310b5d 100644 --- a/csharp.md +++ b/csharp.md @@ -11,6 +11,8 @@ - .Length .Split .Join .Contains etc - string.Equals() string.IsNullOrWhiteSpace() - int.Parse() int.TryParse() +- strings are immutable, new copy is created when concatenating +- StringBuilder() is mutable (faster O(n) than string concatenation O(n^2)) (prefer string.Join()) String formatting: @@ -24,25 +26,38 @@ String formatting: - arrays are fixed while lists are dynamic (size) - defaults: - - int → 0 - - double → 0.0 - - bool → false - - string → null (reference types default to null) - + - int → 0 + - double → 0.0 + - bool → false + - string → null (reference types default to null) - use for loop when you need index otherwise foreach - - or: foreach (string name in names.Select((value, index) => new { value, index })) - + - or: foreach (string name in names.Select((value, index) => new { value, index })) - Array.Sort() Array.Reverse() Array.IndexOf() +- multi-dimensional arrays: + ```cs + int[,] grid = new int[3, 4]; // 3 rows, 4 columns + // GetLength(0) → number of rows + // GetLength(1) → number of columns + ``` +- jagged arrays (array of arrays): + ```cs + int[][] jagged = new int[3][3]; // 3 rows, 4 columns + ``` **Switch** - have to use break/return/throw - possible to stack case labels +- switch expressions (shorthand): + ```cs + return mark switch + { + >= 70 => "A", + >= 60 or => "B", + >= 50 and => "C", + _ => "F", + }; + ``` **List** @@ -51,17 +66,14 @@ String formatting: **OOP** - struct vs class: - - struct creates deep copy when assigned or passed to function (fix by returning new copy in function) - - class is single shared instance (default to classes) - + - struct creates deep copy when assigned or passed to function (fix by returning new copy in function) + - class is single shared instance (default to classes) - protected: not accessible outside (object) the class/inherited classes - always try to keep logic to private variables and keep public variables small, and only expose what you need (so we don't have to change every implementation when refactoring public variable) - abstract class: cannot create object of class -- virtual: +- virtual: declare that class can be overridden by children - use structs for small data, e.g. coordinates, money, date ranges - use classes when data gets more complex than that + - use classes when data gets more complex than that - polymorphism: overriding parent methods **Enums** From 08fb57d6cdcefc51df25e5d92b2dfab843ec72ae Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:18:48 +0200 Subject: [PATCH 06/27] bank --- projects/bank/Bank/Account.cs | 70 +++++++++++++++++++++++++++---- projects/bank/Bank/Bank.cs | 27 ++++++++---- projects/bank/Bank/Program.cs | 16 +++++-- projects/bank/Bank/Transaction.cs | 10 ++++- 4 files changed, 103 insertions(+), 20 deletions(-) diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 2993428..853b746 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -25,34 +25,76 @@ public class Account public Account(string accountNumber, string holder, decimal startingBalance) { - throw new NotImplementedException("TODO: throw ArgumentException if startingBalance < 0. Assign AccountNumber and Holder. Initialise transactions = new List(). If startingBalance > 0, append an opening Credit transaction with description \"Opening deposit\"."); + if (startingBalance < 0) + { + throw new ArgumentException("Starting balance must be greater than 0"); + } + + AccountNumber = accountNumber; + Holder = holder; + transactions = new List(); + if (startingBalance > 0) + { + transactions.Add(new Transaction(TransactionType.Credit, startingBalance, "Opening deposit")); + } } // Computed on every read — there's no stored balance field. public decimal Balance { - get { throw new NotImplementedException("TODO: iterate transactions, add Credit amounts, subtract Debit amounts, return the running total"); } + get + { + decimal total = 0; + foreach (Transaction transaction in transactions) + { + if (transaction.Type == TransactionType.Credit) + { + total += transaction.Amount; + } + else if (transaction.Type == TransactionType.Debit) + { + total -= transaction.Amount; + } + } + + return total; + } } public int TransactionCount { - get { throw new NotImplementedException("TODO: return transactions.Count"); } + get { return transactions.Count; } } // Expose a read-only view — callers can enumerate but not Add/Remove. public IReadOnlyList Transactions { - get { throw new NotImplementedException("TODO: return transactions.AsReadOnly()"); } + get { return transactions.AsReadOnly(); } } public void Deposit(decimal amount) { - throw new NotImplementedException("TODO: throw ArgumentException if amount <= 0, then add a Credit transaction with description \"Deposit\""); + if (amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + transactions.Add(new Transaction(TransactionType.Credit, amount, "Deposit")); } public void Withdraw(decimal amount) { - throw new NotImplementedException("TODO: throw ArgumentException if amount <= 0, throw InvalidOperationException if amount > Balance, otherwise add a Debit transaction with description \"Withdrawal\". On failure the transaction must NOT be recorded."); + if (amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + if (amount > Balance) + { + throw new InvalidOperationException("Amount must be less than or equal to Balance"); + } + + transactions.Add(new Transaction(TransactionType.Debit, amount, "Withdrawal")); } // Returns a printable multi-line bank statement. Format is deliberately @@ -60,13 +102,25 @@ public void Withdraw(decimal amount) // appear in the output, so you're free to make it pretty. public string Statement() { - throw new NotImplementedException("TODO: return a multi-line string — header with AccountNumber, Holder, and Balance; then a row per transaction showing timestamp, CREDIT/DEBIT, amount, and description. See the `decimal` and DateTime mini-lessons in README.md for formatting."); + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"Account Number: {AccountNumber}"); + sb.AppendLine($"Holder: {Holder}"); + sb.AppendLine($"Balance: {Balance:N2}"); + sb.AppendLine("Transactions:"); + foreach (Transaction transaction in transactions) + { + sb.AppendLine( + $"{transaction.Timestamp:yyyy-MM-dd HH:mm} {transaction.Type.ToString().ToUpper()} {transaction.Amount:N2} {transaction.Description}"); + } + + return sb.ToString(); } // Case-insensitive substring match on Description. // Results are sorted oldest-first by Timestamp. public List FindTransactions(string search) { - throw new NotImplementedException("TODO: return every transaction whose Description contains `search` (case-insensitive), sorted by Timestamp oldest-first"); + return transactions.Where(t => t.Description.Contains(search, StringComparison.OrdinalIgnoreCase)) + .OrderBy(t => t.Timestamp).ToList(); } } diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index ca1ab8a..49b9906 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -15,39 +15,52 @@ public class Bank public Bank(string name) { - throw new NotImplementedException("TODO: assign Name, initialise accounts = new List(), and start nextAccountNumber at 1000"); + Name = name; + accounts = new List(); + nextAccountNumber = 1000; } public int AccountCount { - get { throw new NotImplementedException("TODO: return accounts.Count"); } + get { return accounts.Count; } } // Sum of every account's balance at this moment. Computed, not stored. public decimal TotalAssets { - get { throw new NotImplementedException("TODO: iterate accounts and sum each one's Balance"); } + get { return accounts.Sum(a => a.Balance); } } public IReadOnlyList Accounts { - get { throw new NotImplementedException("TODO: return accounts.AsReadOnly()"); } + get { return accounts.AsReadOnly(); } } public Account OpenAccount(string holder, decimal startingBalance) { - throw new NotImplementedException("TODO: format the next account number as $\"ACC-{nextAccountNumber}\", increment the counter, construct an Account, add it to the accounts list, and return it"); + string accountNumber = $"ACC-{nextAccountNumber}"; + nextAccountNumber++; + Account account = new Account(accountNumber, holder, startingBalance); + accounts.Add(account); + return account; } // Returns null when no account matches. public Account? FindAccount(string accountNumber) { - throw new NotImplementedException("TODO: iterate accounts and return the one whose AccountNumber matches; otherwise return null"); + return accounts.FirstOrDefault(a => a.AccountNumber == accountNumber); } // Returns true if an account was found and removed, false otherwise. public bool CloseAccount(string accountNumber) { - throw new NotImplementedException("TODO: find the matching account and remove it from the list; return true if removed, false if not found"); + Account? account = FindAccount(accountNumber); + if (account != null) + { + accounts.Remove(account); + return true; + } + + return false; } } diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index e5b6ffe..cd6a2dd 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -15,10 +15,10 @@ Account alan = bank.OpenAccount("Alan Turing", 3410.00m); // A week of Ada's account activity — all amounts in decimals. -ada.Deposit(89.99m); // payday top-up -ada.Withdraw(12.50m); // lunch -ada.Withdraw(47.33m); // groceries -ada.Deposit(500m); // refund +ada.Deposit(89.99m); // payday top-up +ada.Withdraw(12.50m); // lunch +ada.Withdraw(47.33m); // groceries +ada.Deposit(500m); // refund // Alan's activity, including a big bill. alan.Withdraw(1299.99m); @@ -54,6 +54,14 @@ Console.WriteLine($" {t.Timestamp:yyyy-MM-dd HH:mm} {t.Amount,10:N2} {t.Description}"); } +Console.WriteLine(); +Console.WriteLine("TESTING"); +Account a = new Account("ACC-1000", "Ada", 100m); +a.Deposit(50m); +a.Withdraw(30m); +string s = a.Statement(); +Console.WriteLine(s); + // TODO (students): extend the demo. Ideas — // • Try more edge cases — zero amounts, negative amounts — and catch the // ArgumentException each one throws. diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/Transaction.cs index 7a75e46..e7ce712 100644 --- a/projects/bank/Bank/Transaction.cs +++ b/projects/bank/Bank/Transaction.cs @@ -24,6 +24,14 @@ public struct Transaction public Transaction(TransactionType type, decimal amount, string description) { - throw new NotImplementedException("TODO: throw ArgumentException if amount <= 0, then assign Type, Amount, Description, and set Timestamp = DateTime.UtcNow"); + if (amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + Type = type; + Amount = amount; + Timestamp = DateTime.UtcNow; + Description = description; } } From 80003ffda659c39028fa00dd0876fbc462b6a507 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:08:31 +0200 Subject: [PATCH 07/27] transfer --- projects/bank/Bank.Tests/BankTests.cs | 38 ++++++++++++++++++++++++++- projects/bank/Bank/Account.cs | 8 +++--- projects/bank/Bank/Bank.cs | 23 ++++++++++++++++ projects/bank/Bank/Program.cs | 11 +++++--- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index fa083d6..36d2300 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -51,7 +51,7 @@ public void TotalAssets_SumsEveryAccountsBalance() b.OpenAccount("Ada", 100m); b.OpenAccount("Alan", 250m); Account grace = b.OpenAccount("Grace", 500m); - grace.Withdraw(50m); // Grace now 450 + grace.Withdraw(50m); // Grace now 450 Assert.Equal(800m, b.TotalAssets); // 100 + 250 + 450 } @@ -115,4 +115,40 @@ public void NextAccountNumber_IsInstanceScopedNotShared() Assert.Equal("ACC-1000", a.AccountNumber); Assert.Equal("ACC-1000", c.AccountNumber); } + + [Fact] + public void Transfer_TransfersFundsBetweenAccounts() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenAccount("Ada", 100m); + Account b = bank.OpenAccount("Alan", 200m); + bank.Transfer(a.AccountNumber, b.AccountNumber, 50m); + Assert.Equal(50m, a.Balance); + Assert.Equal(250m, b.Balance); + } + + [Fact] + public void Transfer_ThrowsWhenFromAccountNotFound() + { + Bank bank = new Bank("Acme"); + Account b = bank.OpenAccount("Alan", 200m); + Assert.Throws(() => bank.Transfer("ACC-9999", b.AccountNumber, 50m)); + } + + [Fact] + public void Transfer_ThrowsWhenToAccountNotFound() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenAccount("Ada", 100m); + Assert.Throws(() => bank.Transfer(a.AccountNumber, "ACC-9999", 50m)); + } + + [Fact] + public void Transfer_ThrowsWhenInsufficientFunds() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenAccount("Ada", 100m); + Account b = bank.OpenAccount("Alan", 200m); + Assert.Throws(() => bank.Transfer(a.AccountNumber, b.AccountNumber, 150m)); + } } diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 853b746..1f89b5e 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -72,17 +72,17 @@ public IReadOnlyList Transactions get { return transactions.AsReadOnly(); } } - public void Deposit(decimal amount) + public void Deposit(decimal amount, string description = "Deposit") { if (amount <= 0) { throw new ArgumentException("Amount must be greater than 0"); } - transactions.Add(new Transaction(TransactionType.Credit, amount, "Deposit")); + transactions.Add(new Transaction(TransactionType.Credit, amount, description)); } - public void Withdraw(decimal amount) + public void Withdraw(decimal amount, string description = "Withdrawal") { if (amount <= 0) { @@ -94,7 +94,7 @@ public void Withdraw(decimal amount) throw new InvalidOperationException("Amount must be less than or equal to Balance"); } - transactions.Add(new Transaction(TransactionType.Debit, amount, "Withdrawal")); + transactions.Add(new Transaction(TransactionType.Debit, amount, description)); } // Returns a printable multi-line bank statement. Format is deliberately diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index 49b9906..f287d81 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -63,4 +63,27 @@ public bool CloseAccount(string accountNumber) return false; } + + public void Transfer(string fromAccountNumber, string toAccountNumber, decimal amount) + { + Account? fromAccount = FindAccount(fromAccountNumber); + if (fromAccount == null) + { + throw new InvalidOperationException("From account not found"); + } + + Account? toAccount = FindAccount(toAccountNumber); + if (toAccount == null) + { + throw new InvalidOperationException("To account not found"); + } + + if (fromAccount.Balance < amount) + { + throw new InvalidOperationException($"Insufficient funds in {fromAccountNumber}"); + } + + fromAccount.Withdraw(amount, $"Transfer to {toAccountNumber}"); + toAccount.Deposit(amount, $"Transfer from {fromAccountNumber}"); + } } diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index cd6a2dd..99c3f28 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -56,11 +56,14 @@ Console.WriteLine(); Console.WriteLine("TESTING"); -Account a = new Account("ACC-1000", "Ada", 100m); +Account a = bank.OpenAccount("Ada Lovelace", 200m); +Account b = bank.OpenAccount("Alan Turing", 200m); a.Deposit(50m); -a.Withdraw(30m); -string s = a.Statement(); -Console.WriteLine(s); +a.Withdraw(20m); +b.Deposit(20m); +bank.Transfer(a.AccountNumber, b.AccountNumber, 50m); +Console.WriteLine(a.Statement()); +Console.WriteLine(b.Statement()); // TODO (students): extend the demo. Ideas — // • Try more edge cases — zero amounts, negative amounts — and catch the From 8187dda4dcae2590a9bea1e4691efc79ac71808a Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:37:04 +0200 Subject: [PATCH 08/27] overdraft --- projects/bank/Bank/Account.cs | 15 +++++++++++---- projects/bank/Bank/Bank.cs | 4 ++-- projects/bank/Bank/Program.cs | 4 ++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 1f89b5e..c237274 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -22,8 +22,9 @@ public class Account public string AccountNumber { get; } public string Holder { get; } + public decimal OverdraftLimit { get; } - public Account(string accountNumber, string holder, decimal startingBalance) + public Account(string accountNumber, string holder, decimal startingBalance, decimal overdraftLimit = 0m) { if (startingBalance < 0) { @@ -33,6 +34,7 @@ public Account(string accountNumber, string holder, decimal startingBalance) AccountNumber = accountNumber; Holder = holder; transactions = new List(); + OverdraftLimit = overdraftLimit; if (startingBalance > 0) { transactions.Add(new Transaction(TransactionType.Credit, startingBalance, "Opening deposit")); @@ -79,7 +81,9 @@ public void Deposit(decimal amount, string description = "Deposit") throw new ArgumentException("Amount must be greater than 0"); } - transactions.Add(new Transaction(TransactionType.Credit, amount, description)); + decimal newBalance = Balance + amount; + string desc = description + $" (New Balance: {newBalance:N2})"; + transactions.Add(new Transaction(TransactionType.Credit, amount, desc)); } public void Withdraw(decimal amount, string description = "Withdrawal") @@ -89,12 +93,14 @@ public void Withdraw(decimal amount, string description = "Withdrawal") throw new ArgumentException("Amount must be greater than 0"); } - if (amount > Balance) + if (amount > Balance + OverdraftLimit) { throw new InvalidOperationException("Amount must be less than or equal to Balance"); } - transactions.Add(new Transaction(TransactionType.Debit, amount, description)); + decimal newBalance = Balance - amount; + string desc = description + $" (New Balance: {newBalance:N2})"; + transactions.Add(new Transaction(TransactionType.Debit, amount, desc)); } // Returns a printable multi-line bank statement. Format is deliberately @@ -106,6 +112,7 @@ public string Statement() sb.AppendLine($"Account Number: {AccountNumber}"); sb.AppendLine($"Holder: {Holder}"); sb.AppendLine($"Balance: {Balance:N2}"); + sb.AppendLine($"Overdraft Limit: {OverdraftLimit:N2}"); sb.AppendLine("Transactions:"); foreach (Transaction transaction in transactions) { diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index f287d81..893cdf4 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -36,11 +36,11 @@ public IReadOnlyList Accounts get { return accounts.AsReadOnly(); } } - public Account OpenAccount(string holder, decimal startingBalance) + public Account OpenAccount(string holder, decimal startingBalance, decimal overdraftLimit = 0m) { string accountNumber = $"ACC-{nextAccountNumber}"; nextAccountNumber++; - Account account = new Account(accountNumber, holder, startingBalance); + Account account = new Account(accountNumber, holder, startingBalance, overdraftLimit); accounts.Add(account); return account; } diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 99c3f28..406cc02 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -58,12 +58,16 @@ Console.WriteLine("TESTING"); Account a = bank.OpenAccount("Ada Lovelace", 200m); Account b = bank.OpenAccount("Alan Turing", 200m); +Account c = bank.OpenAccount("Bob Smith", 0m, 100m); +c.Withdraw(50m); a.Deposit(50m); a.Withdraw(20m); b.Deposit(20m); bank.Transfer(a.AccountNumber, b.AccountNumber, 50m); Console.WriteLine(a.Statement()); Console.WriteLine(b.Statement()); +Console.WriteLine(c.Statement()); +Console.WriteLine(bank.TotalAssets); // TODO (students): extend the demo. Ideas — // • Try more edge cases — zero amounts, negative amounts — and catch the From b55644f64941be9a7dab3be2e18743c4c81f88dc Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:49:34 +0200 Subject: [PATCH 09/27] apply interest --- projects/bank/Bank.Tests/BankTests.cs | 23 +++++++++++++++++++++++ projects/bank/Bank/Bank.cs | 8 ++++++++ projects/bank/Bank/Program.cs | 5 +++++ 3 files changed, 36 insertions(+) diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index 36d2300..04a7b59 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -151,4 +151,27 @@ public void Transfer_ThrowsWhenInsufficientFunds() Account b = bank.OpenAccount("Alan", 200m); Assert.Throws(() => bank.Transfer(a.AccountNumber, b.AccountNumber, 150m)); } + + [Fact] + public void ApplyInterest_CreditsInterestToAccountsWithPositiveBalance() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenAccount("Ada", 100m); + Account b = bank.OpenAccount("Alan", 200m); + bank.ApplyInterest(0.05m); + Assert.Equal(105m, a.Balance); + Assert.Equal(210m, b.Balance); + } + + [Fact] + public void ApplyInterest_DoesNotCreditInterestToAccountsWithZeroOrNegativeBalance() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenAccount("Ada", 0m); + Account b = bank.OpenAccount("Alan", 0m, 100m); + b.Withdraw(50m); + bank.ApplyInterest(0.05m); + Assert.Equal(0m, a.Balance); + Assert.Equal(-50m, b.Balance); + } } diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index 893cdf4..c489456 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -86,4 +86,12 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a fromAccount.Withdraw(amount, $"Transfer to {toAccountNumber}"); toAccount.Deposit(amount, $"Transfer from {fromAccountNumber}"); } + + public void ApplyInterest(decimal rate) + { + foreach (Account account in accounts.Where(a => a.Balance > 0)) + { + account.Deposit(account.Balance * rate, $"Interest {rate:P2}"); + } + } } diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 406cc02..8c49b85 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -69,6 +69,11 @@ Console.WriteLine(c.Statement()); Console.WriteLine(bank.TotalAssets); +bank.ApplyInterest(0.05m); +Console.WriteLine(a.Statement()); +Console.WriteLine(b.Statement()); +Console.WriteLine(c.Statement()); + // TODO (students): extend the demo. Ideas — // • Try more edge cases — zero amounts, negative amounts — and catch the // ArgumentException each one throws. From 260b5a9f59d4b87485a21374f499f2fc88f62708 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:15:24 +0200 Subject: [PATCH 10/27] statement in range (no test) --- projects/bank/Bank.Tests/AccountTests.cs | 18 +++++++++++++--- projects/bank/Bank.Tests/TransactionTests.cs | 3 +-- projects/bank/Bank/Account.cs | 22 +++++++++++++++----- projects/bank/Bank/Program.cs | 1 + 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index c959a97..ea5fcac 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -35,8 +35,7 @@ public void Constructor_ZeroStartingBalanceRecordsNoOpeningTransaction() [Fact] public void Constructor_ThrowsOnNegativeStartingBalance() { - Assert.Throws( - () => new Account("ACC-1000", "Ada", -1m)); + Assert.Throws(() => new Account("ACC-1000", "Ada", -1m)); } // ── Balance ──────────────────────────────────────────────────── @@ -104,7 +103,14 @@ public void Withdraw_ThrowsOnInsufficientFunds() public void Withdraw_DoesNotRecordTransactionWhenItFails() { Account a = new Account("ACC-1000", "Ada", 100m); - try { a.Withdraw(500m); } catch (InvalidOperationException) { } + try + { + a.Withdraw(500m); + } + catch (InvalidOperationException) + { + } + Assert.Equal(1, a.TransactionCount); // only the opening deposit Assert.Equal(100m, a.Balance); } @@ -217,4 +223,10 @@ public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() Assert.True(matches[i - 1].Timestamp <= matches[i].Timestamp); } } + + [Fact] + public void Statement_ReturnsTransactionsInRange() + { + // TODO: Mock this somehow + } } diff --git a/projects/bank/Bank.Tests/TransactionTests.cs b/projects/bank/Bank.Tests/TransactionTests.cs index dc39ecd..3f8aab9 100644 --- a/projects/bank/Bank.Tests/TransactionTests.cs +++ b/projects/bank/Bank.Tests/TransactionTests.cs @@ -28,8 +28,7 @@ public void Constructor_StampsTimestampCloseToNow() [InlineData(-100.50)] public void Constructor_ThrowsOnNonPositiveAmount(decimal badAmount) { - Assert.Throws( - () => new Transaction(TransactionType.Credit, badAmount, "x")); + Assert.Throws(() => new Transaction(TransactionType.Credit, badAmount, "x")); } [Fact] diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index c237274..9917541 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -103,10 +103,7 @@ public void Withdraw(decimal amount, string description = "Withdrawal") transactions.Add(new Transaction(TransactionType.Debit, amount, desc)); } - // Returns a printable multi-line bank statement. Format is deliberately - // our own choice here — the tests only check that the required fields - // appear in the output, so you're free to make it pretty. - public string Statement() + private string BuildStatement(List includedTransactions) { StringBuilder sb = new StringBuilder(); sb.AppendLine($"Account Number: {AccountNumber}"); @@ -114,7 +111,7 @@ public string Statement() sb.AppendLine($"Balance: {Balance:N2}"); sb.AppendLine($"Overdraft Limit: {OverdraftLimit:N2}"); sb.AppendLine("Transactions:"); - foreach (Transaction transaction in transactions) + foreach (Transaction transaction in includedTransactions) { sb.AppendLine( $"{transaction.Timestamp:yyyy-MM-dd HH:mm} {transaction.Type.ToString().ToUpper()} {transaction.Amount:N2} {transaction.Description}"); @@ -123,6 +120,21 @@ public string Statement() return sb.ToString(); } + // Returns a printable multi-line bank statement. Format is deliberately + // our own choice here — the tests only check that the required fields + // appear in the output, so you're free to make it pretty. + public string Statement() + { + return BuildStatement(transactions); + } + + public string Statement(DateTime from, DateTime to) + { + List transactionsInRange = + transactions.Where(t => t.Timestamp >= from && t.Timestamp <= to).ToList(); + return BuildStatement(transactionsInRange); + } + // Case-insensitive substring match on Description. // Results are sorted oldest-first by Timestamp. public List FindTransactions(string search) diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 8c49b85..6a8d490 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -70,6 +70,7 @@ Console.WriteLine(bank.TotalAssets); bank.ApplyInterest(0.05m); + Console.WriteLine(a.Statement()); Console.WriteLine(b.Statement()); Console.WriteLine(c.Statement()); From c1637fcc39668fe5da3ba21f8832a9565e2a1eff Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:27:14 +0200 Subject: [PATCH 11/27] class notes --- 04-24.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 04-24.txt diff --git a/04-24.txt b/04-24.txt new file mode 100644 index 0000000..d5edb3e --- /dev/null +++ b/04-24.txt @@ -0,0 +1,15 @@ +- ASP.NET Core Web API +- Enable OpenAPI support +- Use controllers +- IEnumerable = anything that is iterable (list, array, etc) +- ApiDbContext = define tables +- Models = define fields +- Controllers = api routes +- Task = js promise +- seed db in OnModelCreating in ApiDbContext + +Visual Studio: +- Add scaffold +- EntityFrameworkCore.InMemory nuget package +- Scalar.AspNetCore nuget package - generate docs from OpenAPI - browser API reference +- Endpoints explorer \ No newline at end of file From 82af6e885ebec90136abe8f2ea7b5a7d6e126abd Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:50:44 +0200 Subject: [PATCH 12/27] timestamp test --- projects/bank/Bank.Tests/AccountTests.cs | 25 +++++++++++++++++++----- projects/bank/Bank/Account.cs | 14 +++++++++++-- projects/bank/Bank/Transaction.cs | 7 ++++++- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index ea5fcac..c29c80e 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -1,5 +1,3 @@ -using BankApp; - namespace BankApp.Tests; public class AccountTests @@ -130,6 +128,7 @@ public void Withdraw_ThrowsOnNonPositiveAmount(decimal badAmount) public void Transactions_IsReadOnlyView() { Account a = new Account("ACC-1000", "Ada", 100m); + // The returned collection is IReadOnlyList — there is no Add method, // so callers cannot bypass Deposit / Withdraw to mutate history. Assert.IsAssignableFrom>(a.Transactions); @@ -180,6 +179,7 @@ public void FindTransactions_ReturnsAllMatchesByDescription() a.Deposit(50m); a.Withdraw(30m); a.Deposit(20m); + // "Deposit" should match both "Opening deposit" (case-insensitive) // and the two "Deposit" entries. List matches = a.FindTransactions("deposit"); @@ -210,10 +210,11 @@ public void FindTransactions_ReturnsEmptyListWhenNothingMatches() public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() { Account a = new Account("ACC-1000", "Ada", 100m); + // Small sleeps ensure distinct timestamps so the sort is verifiable. - System.Threading.Thread.Sleep(15); + Thread.Sleep(15); a.Deposit(50m); - System.Threading.Thread.Sleep(15); + Thread.Sleep(15); a.Deposit(20m); List matches = a.FindTransactions("deposit"); @@ -227,6 +228,20 @@ public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() [Fact] public void Statement_ReturnsTransactionsInRange() { - // TODO: Mock this somehow + Account a = new Account("ACC-1000", "Ada", 0m); + DateTime from = new DateTime(2026, 1, 10, 0, 0, 0, DateTimeKind.Utc); + DateTime to = new DateTime(2026, 1, 15, 23, 59, 59, DateTimeKind.Utc); + + a.Deposit(25m, new DateTime(2026, 1, 5, 9, 0, 0, DateTimeKind.Utc), "Too early"); + a.Deposit(50m, new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc), "In range deposit"); + a.Withdraw(20m, new DateTime(2026, 1, 15, 18, 30, 0, DateTimeKind.Utc), "In range withdrawal"); + a.Deposit(10m, new DateTime(2026, 1, 20, 9, 0, 0, DateTimeKind.Utc), "Too late"); + + string s = a.Statement(from, to); + + Assert.Contains("In range deposit", s); + Assert.Contains("In range withdrawal", s); + Assert.DoesNotContain("Too early", s); + Assert.DoesNotContain("Too late", s); } } diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 9917541..cb88684 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -75,6 +75,11 @@ public IReadOnlyList Transactions } public void Deposit(decimal amount, string description = "Deposit") + { + Deposit(amount, DateTime.UtcNow, description); + } + + public void Deposit(decimal amount, DateTime timestamp, string description = "Deposit") { if (amount <= 0) { @@ -83,10 +88,15 @@ public void Deposit(decimal amount, string description = "Deposit") decimal newBalance = Balance + amount; string desc = description + $" (New Balance: {newBalance:N2})"; - transactions.Add(new Transaction(TransactionType.Credit, amount, desc)); + transactions.Add(new Transaction(TransactionType.Credit, amount, timestamp, desc)); } public void Withdraw(decimal amount, string description = "Withdrawal") + { + Withdraw(amount, DateTime.UtcNow, description); + } + + public void Withdraw(decimal amount, DateTime timestamp, string description = "Withdrawal") { if (amount <= 0) { @@ -100,7 +110,7 @@ public void Withdraw(decimal amount, string description = "Withdrawal") decimal newBalance = Balance - amount; string desc = description + $" (New Balance: {newBalance:N2})"; - transactions.Add(new Transaction(TransactionType.Debit, amount, desc)); + transactions.Add(new Transaction(TransactionType.Debit, amount, timestamp, desc)); } private string BuildStatement(List includedTransactions) diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/Transaction.cs index e7ce712..9bd7d09 100644 --- a/projects/bank/Bank/Transaction.cs +++ b/projects/bank/Bank/Transaction.cs @@ -23,6 +23,11 @@ public struct Transaction public string Description { get; } public Transaction(TransactionType type, decimal amount, string description) + : this(type, amount, DateTime.UtcNow, description) + { + } + + public Transaction(TransactionType type, decimal amount, DateTime timestamp, string description) { if (amount <= 0) { @@ -31,7 +36,7 @@ public Transaction(TransactionType type, decimal amount, string description) Type = type; Amount = amount; - Timestamp = DateTime.UtcNow; + Timestamp = timestamp; Description = description; } } From 6b407d020697f7a410db40d20a5167559c70f095 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:00:31 +0200 Subject: [PATCH 13/27] clean --- projects/bank/Bank.Tests/BankTests.cs | 2 - projects/bank/Bank.Tests/TransactionTests.cs | 2 - projects/bank/Bank/Account.cs | 49 ++++----------- projects/bank/Bank/Bank.cs | 41 ++++--------- projects/bank/Bank/Program.cs | 64 +------------------- projects/bank/Bank/Transaction.cs | 15 ----- projects/bank/Bank/TransactionType.cs | 9 +-- 7 files changed, 27 insertions(+), 155 deletions(-) diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index 04a7b59..f12c388 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -1,5 +1,3 @@ -using BankApp; - namespace BankApp.Tests; public class BankTests diff --git a/projects/bank/Bank.Tests/TransactionTests.cs b/projects/bank/Bank.Tests/TransactionTests.cs index 3f8aab9..4d0a982 100644 --- a/projects/bank/Bank.Tests/TransactionTests.cs +++ b/projects/bank/Bank.Tests/TransactionTests.cs @@ -1,5 +1,3 @@ -using BankApp; - namespace BankApp.Tests; public class TransactionTests diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index cb88684..2fae7ef 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -2,23 +2,9 @@ namespace BankApp; -// A bank account belonging to one holder. Keeps its own transaction history -// and computes its balance from that history (nothing else stores a balance). -// -// Key rules: -// • The opening balance passed to the constructor is recorded as the FIRST -// transaction (a Credit with description "Opening deposit") — UNLESS -// startingBalance is exactly 0, in which case the history starts empty. -// • Balance is derived: sum of Credits minus sum of Debits. -// • Deposit and Withdraw both require a positive amount (ArgumentException -// otherwise). Withdraw throws InvalidOperationException if the amount -// would take the balance below zero (no overdrafts in the core spec). -// -// The `transactions` list is private so outside code can only change the -// account through Deposit / Withdraw — the balance invariant stays intact. public class Account { - private List transactions; + private readonly List _transactions; public string AccountNumber { get; } public string Holder { get; } @@ -33,21 +19,20 @@ public Account(string accountNumber, string holder, decimal startingBalance, dec AccountNumber = accountNumber; Holder = holder; - transactions = new List(); + _transactions = new List(); OverdraftLimit = overdraftLimit; if (startingBalance > 0) { - transactions.Add(new Transaction(TransactionType.Credit, startingBalance, "Opening deposit")); + _transactions.Add(new Transaction(TransactionType.Credit, startingBalance, "Opening deposit")); } } - // Computed on every read — there's no stored balance field. public decimal Balance { get { decimal total = 0; - foreach (Transaction transaction in transactions) + foreach (Transaction transaction in _transactions) { if (transaction.Type == TransactionType.Credit) { @@ -63,16 +48,9 @@ public decimal Balance } } - public int TransactionCount - { - get { return transactions.Count; } - } + public int TransactionCount => _transactions.Count; - // Expose a read-only view — callers can enumerate but not Add/Remove. - public IReadOnlyList Transactions - { - get { return transactions.AsReadOnly(); } - } + public IReadOnlyList Transactions => _transactions.AsReadOnly(); public void Deposit(decimal amount, string description = "Deposit") { @@ -88,7 +66,7 @@ public void Deposit(decimal amount, DateTime timestamp, string description = "De decimal newBalance = Balance + amount; string desc = description + $" (New Balance: {newBalance:N2})"; - transactions.Add(new Transaction(TransactionType.Credit, amount, timestamp, desc)); + _transactions.Add(new Transaction(TransactionType.Credit, amount, timestamp, desc)); } public void Withdraw(decimal amount, string description = "Withdrawal") @@ -110,7 +88,7 @@ public void Withdraw(decimal amount, DateTime timestamp, string description = "W decimal newBalance = Balance - amount; string desc = description + $" (New Balance: {newBalance:N2})"; - transactions.Add(new Transaction(TransactionType.Debit, amount, timestamp, desc)); + _transactions.Add(new Transaction(TransactionType.Debit, amount, timestamp, desc)); } private string BuildStatement(List includedTransactions) @@ -130,26 +108,21 @@ private string BuildStatement(List includedTransactions) return sb.ToString(); } - // Returns a printable multi-line bank statement. Format is deliberately - // our own choice here — the tests only check that the required fields - // appear in the output, so you're free to make it pretty. public string Statement() { - return BuildStatement(transactions); + return BuildStatement(_transactions); } public string Statement(DateTime from, DateTime to) { List transactionsInRange = - transactions.Where(t => t.Timestamp >= from && t.Timestamp <= to).ToList(); + _transactions.Where(t => t.Timestamp >= from && t.Timestamp <= to).ToList(); return BuildStatement(transactionsInRange); } - // Case-insensitive substring match on Description. - // Results are sorted oldest-first by Timestamp. public List FindTransactions(string search) { - return transactions.Where(t => t.Description.Contains(search, StringComparison.OrdinalIgnoreCase)) + return _transactions.Where(t => t.Description.Contains(search, StringComparison.OrdinalIgnoreCase)) .OrderBy(t => t.Timestamp).ToList(); } } diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index c489456..eb2a662 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -1,63 +1,48 @@ namespace BankApp; -// The top-level simulation: one Bank holds many Accounts, generates account -// numbers, and exposes summary info across the whole portfolio. -// -// Account numbers are auto-generated in the form "ACC-1000", "ACC-1001", ... -// The next-number counter lives as an instance field on the Bank (one counter -// per Bank instance). Simple, private, no `static` required. public class Bank { - private List accounts; - private int nextAccountNumber; + private readonly List _accounts; + private int _nextAccountNumber; public string Name { get; } public Bank(string name) { Name = name; - accounts = new List(); - nextAccountNumber = 1000; + _accounts = new List(); + _nextAccountNumber = 1000; } - public int AccountCount - { - get { return accounts.Count; } - } + public int AccountCount => _accounts.Count; - // Sum of every account's balance at this moment. Computed, not stored. public decimal TotalAssets { - get { return accounts.Sum(a => a.Balance); } + get { return _accounts.Sum(a => a.Balance); } } - public IReadOnlyList Accounts - { - get { return accounts.AsReadOnly(); } - } + public IReadOnlyList Accounts => _accounts.AsReadOnly(); public Account OpenAccount(string holder, decimal startingBalance, decimal overdraftLimit = 0m) { - string accountNumber = $"ACC-{nextAccountNumber}"; - nextAccountNumber++; + string accountNumber = $"ACC-{_nextAccountNumber}"; + _nextAccountNumber++; Account account = new Account(accountNumber, holder, startingBalance, overdraftLimit); - accounts.Add(account); + _accounts.Add(account); return account; } - // Returns null when no account matches. public Account? FindAccount(string accountNumber) { - return accounts.FirstOrDefault(a => a.AccountNumber == accountNumber); + return _accounts.FirstOrDefault(a => a.AccountNumber == accountNumber); } - // Returns true if an account was found and removed, false otherwise. public bool CloseAccount(string accountNumber) { Account? account = FindAccount(accountNumber); if (account != null) { - accounts.Remove(account); + _accounts.Remove(account); return true; } @@ -89,7 +74,7 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a public void ApplyInterest(decimal rate) { - foreach (Account account in accounts.Where(a => a.Balance > 0)) + foreach (Account account in _accounts.Where(a => a.Balance > 0)) { account.Deposit(account.Balance * rate, $"Interest {rate:P2}"); } diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 6a8d490..513278f 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -1,61 +1,6 @@ using BankApp; -// Demo scaffolding — this is here so you can see the domain working end-to-end -// when you press F5. Students: extend this with your own scenarios once the -// tests are green. -// -// Note the `m` suffix on every amount literal — that marks it as a `decimal`. -// For the full rundown on why money uses `decimal` (and not `double` / `float`), -// how to format amounts, and how to round them, see the -// "`decimal` — mini-lesson" section in README.md. - Bank bank = new Bank("Acme Savings"); - -Account ada = bank.OpenAccount("Ada Lovelace", 1250.75m); -Account alan = bank.OpenAccount("Alan Turing", 3410.00m); - -// A week of Ada's account activity — all amounts in decimals. -ada.Deposit(89.99m); // payday top-up -ada.Withdraw(12.50m); // lunch -ada.Withdraw(47.33m); // groceries -ada.Deposit(500m); // refund - -// Alan's activity, including a big bill. -alan.Withdraw(1299.99m); -alan.Deposit(42.42m); -alan.Withdraw(2000m); - -// Try to overdraw — the account throws InvalidOperationException, -// we catch it so the program keeps running. -try -{ - alan.Withdraw(10_000m); -} -catch (InvalidOperationException ex) -{ - Console.WriteLine($"Blocked withdrawal on {alan.AccountNumber}: {ex.Message}"); - Console.WriteLine(); -} - -Console.WriteLine($"Bank: {bank.Name}"); -Console.WriteLine($"Accounts open: {bank.AccountCount}"); -Console.WriteLine($"Total assets: {bank.TotalAssets:N2}"); -Console.WriteLine(); - -Console.WriteLine(ada.Statement()); -Console.WriteLine(); -Console.WriteLine(alan.Statement()); - -// Show FindTransactions — look up every "deposit" on Ada's account. -Console.WriteLine(); -Console.WriteLine($"Ada's deposits:"); -foreach (Transaction t in ada.FindTransactions("deposit")) -{ - Console.WriteLine($" {t.Timestamp:yyyy-MM-dd HH:mm} {t.Amount,10:N2} {t.Description}"); -} - -Console.WriteLine(); -Console.WriteLine("TESTING"); Account a = bank.OpenAccount("Ada Lovelace", 200m); Account b = bank.OpenAccount("Alan Turing", 200m); Account c = bank.OpenAccount("Bob Smith", 0m, 100m); @@ -73,11 +18,4 @@ Console.WriteLine(a.Statement()); Console.WriteLine(b.Statement()); -Console.WriteLine(c.Statement()); - -// TODO (students): extend the demo. Ideas — -// • Try more edge cases — zero amounts, negative amounts — and catch the -// ArgumentException each one throws. -// • Close an account and print the updated bank totals. -// • Add a third customer with their own sequence of transactions. -// • Implement one of the Extensions from the README (Transfer, Overdraft…). +Console.WriteLine(c.Statement()); \ No newline at end of file diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/Transaction.cs index 9bd7d09..66356f2 100644 --- a/projects/bank/Bank/Transaction.cs +++ b/projects/bank/Bank/Transaction.cs @@ -1,20 +1,5 @@ namespace BankApp; -// A single movement of money against an Account. -// -// Transactions are a STRUCT — they're small, immutable records of "what -// happened, when, and why". Once created, none of their fields change. -// An account's transaction history is just a list of these. -// -// Fields: -// Type Credit or Debit (see TransactionType) -// Amount Always POSITIVE. The Type decides whether it adds or subtracts. -// Timestamp When the transaction was recorded (UTC). -// Description Human-readable label, e.g. "Opening deposit", "Withdrawal". -// -// Why positive-only `Amount` and a separate `Type`? It keeps validation -// simple ("amount must be > 0") and makes the transaction record read -// clearly when printed. public struct Transaction { public TransactionType Type { get; } diff --git a/projects/bank/Bank/TransactionType.cs b/projects/bank/Bank/TransactionType.cs index a464631..84e0fd7 100644 --- a/projects/bank/Bank/TransactionType.cs +++ b/projects/bank/Bank/TransactionType.cs @@ -1,12 +1,7 @@ namespace BankApp; -// Two kinds of transaction against an account. -// Credit → money INTO the account (starting deposit, deposits) -// Debit → money OUT of the account (withdrawals) -// -// Balance is computed by summing Credits and subtracting Debits. public enum TransactionType { - Credit, - Debit + Credit, // Credit → money INTO the account (starting deposit, deposits) + Debit // Debit → money OUT of the account (withdrawals) } From acb51373f22ce4fdbf22d07050b730221e7f1690 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:31:49 +0200 Subject: [PATCH 14/27] transaction category --- projects/bank/Bank.Tests/AccountTests.cs | 11 +++++--- projects/bank/Bank.Tests/TransactionTests.cs | 7 ++--- projects/bank/Bank/Account.cs | 28 +++++++++++++------- projects/bank/Bank/Bank.cs | 6 ++--- projects/bank/Bank/Transaction.cs | 15 ++++++++--- projects/bank/Bank/TransactionCategory.cs | 12 +++++++++ 6 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 projects/bank/Bank/TransactionCategory.cs diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index c29c80e..d17ebce 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -225,6 +225,7 @@ public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() } } + [Fact] public void Statement_ReturnsTransactionsInRange() { @@ -232,10 +233,12 @@ public void Statement_ReturnsTransactionsInRange() DateTime from = new DateTime(2026, 1, 10, 0, 0, 0, DateTimeKind.Utc); DateTime to = new DateTime(2026, 1, 15, 23, 59, 59, DateTimeKind.Utc); - a.Deposit(25m, new DateTime(2026, 1, 5, 9, 0, 0, DateTimeKind.Utc), "Too early"); - a.Deposit(50m, new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc), "In range deposit"); - a.Withdraw(20m, new DateTime(2026, 1, 15, 18, 30, 0, DateTimeKind.Utc), "In range withdrawal"); - a.Deposit(10m, new DateTime(2026, 1, 20, 9, 0, 0, DateTimeKind.Utc), "Too late"); + a.Deposit(25m, new DateTime(2026, 1, 5, 9, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, "Too early"); + a.Deposit(50m, new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, + "In range deposit"); + a.Withdraw(20m, new DateTime(2026, 1, 15, 18, 30, 0, DateTimeKind.Utc), TransactionCategory.Other, + "In range withdrawal"); + a.Deposit(10m, new DateTime(2026, 1, 20, 9, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, "Too late"); string s = a.Statement(from, to); diff --git a/projects/bank/Bank.Tests/TransactionTests.cs b/projects/bank/Bank.Tests/TransactionTests.cs index 4d0a982..15ee33a 100644 --- a/projects/bank/Bank.Tests/TransactionTests.cs +++ b/projects/bank/Bank.Tests/TransactionTests.cs @@ -5,7 +5,7 @@ public class TransactionTests [Fact] public void Constructor_AssignsAllProperties() { - Transaction t = new Transaction(TransactionType.Credit, 100m, "Opening deposit"); + Transaction t = new Transaction(TransactionType.Credit, 100m, TransactionCategory.Other, "Opening deposit"); Assert.Equal(TransactionType.Credit, t.Type); Assert.Equal(100m, t.Amount); Assert.Equal("Opening deposit", t.Description); @@ -15,7 +15,7 @@ public void Constructor_AssignsAllProperties() public void Constructor_StampsTimestampCloseToNow() { DateTime before = DateTime.UtcNow; - Transaction t = new Transaction(TransactionType.Credit, 1m, "x"); + Transaction t = new Transaction(TransactionType.Credit, 1m, TransactionCategory.Other, "x"); DateTime after = DateTime.UtcNow; Assert.InRange(t.Timestamp, before, after); } @@ -26,7 +26,8 @@ public void Constructor_StampsTimestampCloseToNow() [InlineData(-100.50)] public void Constructor_ThrowsOnNonPositiveAmount(decimal badAmount) { - Assert.Throws(() => new Transaction(TransactionType.Credit, badAmount, "x")); + Assert.Throws(() => + new Transaction(TransactionType.Credit, badAmount, TransactionCategory.Other, "x")); } [Fact] diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 2fae7ef..9342519 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -23,7 +23,8 @@ public Account(string accountNumber, string holder, decimal startingBalance, dec OverdraftLimit = overdraftLimit; if (startingBalance > 0) { - _transactions.Add(new Transaction(TransactionType.Credit, startingBalance, "Opening deposit")); + _transactions.Add(new Transaction(TransactionType.Credit, startingBalance, TransactionCategory.Other, + "Opening deposit")); } } @@ -52,12 +53,14 @@ public decimal Balance public IReadOnlyList Transactions => _transactions.AsReadOnly(); - public void Deposit(decimal amount, string description = "Deposit") + public void Deposit(decimal amount, TransactionCategory category = TransactionCategory.Other, + string description = "Deposit") { - Deposit(amount, DateTime.UtcNow, description); + Deposit(amount, DateTime.UtcNow, category, description); } - public void Deposit(decimal amount, DateTime timestamp, string description = "Deposit") + public void Deposit(decimal amount, DateTime timestamp, TransactionCategory category = TransactionCategory.Other, + string description = "Deposit") { if (amount <= 0) { @@ -66,15 +69,17 @@ public void Deposit(decimal amount, DateTime timestamp, string description = "De decimal newBalance = Balance + amount; string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Credit, amount, timestamp, desc)); + _transactions.Add(new Transaction(TransactionType.Credit, amount, category, timestamp, desc)); } - public void Withdraw(decimal amount, string description = "Withdrawal") + public void Withdraw(decimal amount, TransactionCategory category = TransactionCategory.Other, + string description = "Withdrawal") { - Withdraw(amount, DateTime.UtcNow, description); + Withdraw(amount, DateTime.UtcNow, category, description); } - public void Withdraw(decimal amount, DateTime timestamp, string description = "Withdrawal") + public void Withdraw(decimal amount, DateTime timestamp, TransactionCategory category = TransactionCategory.Other, + string description = "Withdrawal") { if (amount <= 0) { @@ -88,7 +93,7 @@ public void Withdraw(decimal amount, DateTime timestamp, string description = "W decimal newBalance = Balance - amount; string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Debit, amount, timestamp, desc)); + _transactions.Add(new Transaction(TransactionType.Debit, amount, category, timestamp, desc)); } private string BuildStatement(List includedTransactions) @@ -125,4 +130,9 @@ public List FindTransactions(string search) return _transactions.Where(t => t.Description.Contains(search, StringComparison.OrdinalIgnoreCase)) .OrderBy(t => t.Timestamp).ToList(); } + + public List FindTransactions(TransactionCategory category) + { + return _transactions.Where(t => t.Category == category).ToList(); + } } diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index eb2a662..543f5ba 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -68,15 +68,15 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a throw new InvalidOperationException($"Insufficient funds in {fromAccountNumber}"); } - fromAccount.Withdraw(amount, $"Transfer to {toAccountNumber}"); - toAccount.Deposit(amount, $"Transfer from {fromAccountNumber}"); + fromAccount.Withdraw(amount, TransactionCategory.Transfer, $"Transfer to {toAccountNumber}"); + toAccount.Deposit(amount, TransactionCategory.Transfer, $"Transfer from {fromAccountNumber}"); } public void ApplyInterest(decimal rate) { foreach (Account account in _accounts.Where(a => a.Balance > 0)) { - account.Deposit(account.Balance * rate, $"Interest {rate:P2}"); + account.Deposit(account.Balance * rate, TransactionCategory.Interest, $"Interest {rate:P2}"); } } } diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/Transaction.cs index 66356f2..133b924 100644 --- a/projects/bank/Bank/Transaction.cs +++ b/projects/bank/Bank/Transaction.cs @@ -4,15 +4,21 @@ public struct Transaction { public TransactionType Type { get; } public decimal Amount { get; } + public TransactionCategory Category { get; } public DateTime Timestamp { get; } public string Description { get; } - public Transaction(TransactionType type, decimal amount, string description) - : this(type, amount, DateTime.UtcNow, description) + public Transaction(TransactionType type, decimal amount, TransactionCategory category, string description) + : this(type, amount, category, DateTime.UtcNow, description) { } - public Transaction(TransactionType type, decimal amount, DateTime timestamp, string description) + public Transaction( + TransactionType type, + decimal amount, + TransactionCategory category, + DateTime timestamp, + string description) { if (amount <= 0) { @@ -21,7 +27,8 @@ public Transaction(TransactionType type, decimal amount, DateTime timestamp, str Type = type; Amount = amount; + Category = category; Timestamp = timestamp; Description = description; } -} +} \ No newline at end of file diff --git a/projects/bank/Bank/TransactionCategory.cs b/projects/bank/Bank/TransactionCategory.cs new file mode 100644 index 0000000..1da295a --- /dev/null +++ b/projects/bank/Bank/TransactionCategory.cs @@ -0,0 +1,12 @@ +namespace BankApp; + +public enum TransactionCategory +{ + Food, + Rent, + Salary, + Transfer, + Interest, + Fees, + Other +} From 6c2e3d910c7bf2ede649157e67aa3ea20bfe62fb Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 17:00:15 +0200 Subject: [PATCH 15/27] InsufficientFundsException --- projects/bank/Bank.Tests/AccountTests.cs | 4 ++-- projects/bank/Bank.Tests/BankTests.cs | 2 +- projects/bank/Bank/Account.cs | 5 +++-- projects/bank/Bank/Bank.cs | 3 ++- projects/bank/Bank/Exceptions.cs | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 projects/bank/Bank/Exceptions.cs diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index d17ebce..040f11c 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -94,7 +94,7 @@ public void Withdraw_AddsDebitTransaction() public void Withdraw_ThrowsOnInsufficientFunds() { Account a = new Account("ACC-1000", "Ada", 100m); - Assert.Throws(() => a.Withdraw(500m)); + Assert.Throws(() => a.Withdraw(500m)); } [Fact] @@ -105,7 +105,7 @@ public void Withdraw_DoesNotRecordTransactionWhenItFails() { a.Withdraw(500m); } - catch (InvalidOperationException) + catch (InsufficientFundsException) { } diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index f12c388..97c5be5 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -147,7 +147,7 @@ public void Transfer_ThrowsWhenInsufficientFunds() Bank bank = new Bank("Acme"); Account a = bank.OpenAccount("Ada", 100m); Account b = bank.OpenAccount("Alan", 200m); - Assert.Throws(() => bank.Transfer(a.AccountNumber, b.AccountNumber, 150m)); + Assert.Throws(() => bank.Transfer(a.AccountNumber, b.AccountNumber, 150m)); } [Fact] diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 9342519..e3ba42b 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -5,6 +5,7 @@ namespace BankApp; public class Account { private readonly List _transactions; + private decimal AvailableBalance => Balance + OverdraftLimit; public string AccountNumber { get; } public string Holder { get; } @@ -86,9 +87,9 @@ public void Withdraw(decimal amount, DateTime timestamp, TransactionCategory cat throw new ArgumentException("Amount must be greater than 0"); } - if (amount > Balance + OverdraftLimit) + if (amount > AvailableBalance) { - throw new InvalidOperationException("Amount must be less than or equal to Balance"); + throw new InsufficientFundsException(amount, AvailableBalance); } decimal newBalance = Balance - amount; diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index 543f5ba..ff2535e 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -65,7 +65,8 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a if (fromAccount.Balance < amount) { - throw new InvalidOperationException($"Insufficient funds in {fromAccountNumber}"); + throw new InsufficientFundsException(amount, fromAccount.Balance, + $"Insufficient funds in {fromAccountNumber}"); } fromAccount.Withdraw(amount, TransactionCategory.Transfer, $"Transfer to {toAccountNumber}"); diff --git a/projects/bank/Bank/Exceptions.cs b/projects/bank/Bank/Exceptions.cs new file mode 100644 index 0000000..b0151e8 --- /dev/null +++ b/projects/bank/Bank/Exceptions.cs @@ -0,0 +1,14 @@ +namespace BankApp; + +public class InsufficientFundsException : Exception +{ + public decimal RequestedAmount { get; } + public decimal AvailableBalance { get; } + + public InsufficientFundsException(decimal requestedAmount, decimal availableBalance, + string message = "Insufficient funds") : base(message) + { + RequestedAmount = requestedAmount; + AvailableBalance = availableBalance; + } +} \ No newline at end of file From 589384342dffc69b88a911d5dc0240c51d147323 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 17:40:51 +0200 Subject: [PATCH 16/27] generic Ledger --- projects/bank/Bank/Account.cs | 8 ++++---- projects/bank/Bank/Ledger.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 projects/bank/Bank/Ledger.cs diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index e3ba42b..610d992 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -4,7 +4,7 @@ namespace BankApp; public class Account { - private readonly List _transactions; + private readonly Ledger _transactions; private decimal AvailableBalance => Balance + OverdraftLimit; public string AccountNumber { get; } @@ -20,7 +20,7 @@ public Account(string accountNumber, string holder, decimal startingBalance, dec AccountNumber = accountNumber; Holder = holder; - _transactions = new List(); + _transactions = new Ledger(); OverdraftLimit = overdraftLimit; if (startingBalance > 0) { @@ -52,7 +52,7 @@ public decimal Balance public int TransactionCount => _transactions.Count; - public IReadOnlyList Transactions => _transactions.AsReadOnly(); + public IReadOnlyList Transactions => _transactions.Entries; public void Deposit(decimal amount, TransactionCategory category = TransactionCategory.Other, string description = "Deposit") @@ -97,7 +97,7 @@ public void Withdraw(decimal amount, DateTime timestamp, TransactionCategory cat _transactions.Add(new Transaction(TransactionType.Debit, amount, category, timestamp, desc)); } - private string BuildStatement(List includedTransactions) + private string BuildStatement(IEnumerable includedTransactions) { StringBuilder sb = new StringBuilder(); sb.AppendLine($"Account Number: {AccountNumber}"); diff --git a/projects/bank/Bank/Ledger.cs b/projects/bank/Bank/Ledger.cs new file mode 100644 index 0000000..9c47032 --- /dev/null +++ b/projects/bank/Bank/Ledger.cs @@ -0,0 +1,27 @@ +using System.Collections; + +namespace BankApp; + +public class Ledger : IEnumerable +{ + private readonly List _entries = new List(); + + public int Count => _entries.Count; + + public IReadOnlyList Entries => _entries.AsReadOnly(); + + public void Add(T entry) + { + _entries.Add(entry); + } + + public IEnumerator GetEnumerator() + { + return _entries.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file From 47bc46a41165b7337770016254b36e5118944013 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:11:41 +0200 Subject: [PATCH 17/27] SavingsAccount + CurrentAccount --- projects/bank/Bank.Tests/AccountTests.cs | 36 +++++++++ projects/bank/Bank.Tests/BankTests.cs | 94 +++++++++++++++--------- projects/bank/Bank/Account.cs | 54 +++++++++----- projects/bank/Bank/Bank.cs | 23 +++--- projects/bank/Bank/CurrentAccount.cs | 35 +++++++++ projects/bank/Bank/Program.cs | 6 +- projects/bank/Bank/SavingsAccount.cs | 24 ++++++ 7 files changed, 204 insertions(+), 68 deletions(-) create mode 100644 projects/bank/Bank/CurrentAccount.cs create mode 100644 projects/bank/Bank/SavingsAccount.cs diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index 040f11c..f88af45 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -122,6 +122,42 @@ public void Withdraw_ThrowsOnNonPositiveAmount(decimal badAmount) Assert.Throws(() => a.Withdraw(badAmount)); } + [Fact] + public void ApplyInterest_OnBaseAccount_DoesNothing() + { + Account a = new Account("ACC-1000", "Ada", 100m); + a.ApplyInterest(0.05m); + Assert.Equal(100m, a.Balance); + Assert.Equal(1, a.TransactionCount); + } + + [Fact] + public void SavingsAccount_ApplyInterest_AddsInterestCreditTransaction() + { + SavingsAccount a = new SavingsAccount("ACC-1000", "Ada", 100m); + a.ApplyInterest(0.05m); + Assert.Equal(105m, a.Balance); + Assert.Equal(2, a.TransactionCount); + Assert.Equal(TransactionType.Credit, a.Transactions[1].Type); + Assert.Equal(TransactionCategory.Interest, a.Transactions[1].Category); + } + + [Fact] + public void CurrentAccount_Withdraw_AllowsBalanceToGoNegativeUpToOverdraftLimit() + { + CurrentAccount a = new CurrentAccount("ACC-1000", "Ada", 100m, 50m); + a.Withdraw(125m); + Assert.Equal(-25m, a.Balance); + Assert.Equal(50m, a.OverdraftLimit); + } + + [Fact] + public void CurrentAccount_Withdraw_ThrowsWhenOverdraftLimitWouldBeExceeded() + { + CurrentAccount a = new CurrentAccount("ACC-1000", "Ada", 100m, 50m); + Assert.Throws(() => a.Withdraw(151m)); + } + // ── Transactions (read-only view) ────────────────────────────── [Fact] diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index 97c5be5..d53a96a 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -12,33 +12,44 @@ public void Constructor_AssignsNameAndStartsEmpty() } [Fact] - public void OpenAccount_ReturnsAccountWithAutoAssignedNumber() + public void OpenSavingsAccount_ReturnsSavingsAccountWithAutoAssignedNumber() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); + SavingsAccount a = b.OpenSavingsAccount("Ada", 100m); Assert.Equal("ACC-1000", a.AccountNumber); Assert.Equal("Ada", a.Holder); Assert.Equal(100m, a.Balance); } [Fact] - public void OpenAccount_IncrementsAccountNumbers() + public void OpenCurrentAccount_ReturnsCurrentAccountWithOverdraftLimit() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); - Account c = b.OpenAccount("Alan", 200m); - Account d = b.OpenAccount("Grace", 300m); + CurrentAccount a = b.OpenCurrentAccount("Ada", 100m, 250m); + Assert.Equal("ACC-1000", a.AccountNumber); + Assert.Equal("Ada", a.Holder); + Assert.Equal(100m, a.Balance); + Assert.Equal(250m, a.OverdraftLimit); + } + + [Fact] + public void OpenAccountMethods_IncrementAccountNumbers() + { + Bank b = new Bank("Acme"); + Account a = b.OpenSavingsAccount("Ada", 100m); + Account c = b.OpenCurrentAccount("Alan", 200m, 50m); + Account d = b.OpenSavingsAccount("Grace", 300m); Assert.Equal("ACC-1000", a.AccountNumber); Assert.Equal("ACC-1001", c.AccountNumber); Assert.Equal("ACC-1002", d.AccountNumber); } [Fact] - public void OpenAccount_StoresTheAccountInTheBank() + public void OpenAccountMethods_StoreTheAccountsInTheBank() { Bank b = new Bank("Acme"); - b.OpenAccount("Ada", 100m); - b.OpenAccount("Alan", 200m); + b.OpenSavingsAccount("Ada", 100m); + b.OpenCurrentAccount("Alan", 200m, 100m); Assert.Equal(2, b.AccountCount); } @@ -46,9 +57,9 @@ public void OpenAccount_StoresTheAccountInTheBank() public void TotalAssets_SumsEveryAccountsBalance() { Bank b = new Bank("Acme"); - b.OpenAccount("Ada", 100m); - b.OpenAccount("Alan", 250m); - Account grace = b.OpenAccount("Grace", 500m); + b.OpenSavingsAccount("Ada", 100m); + b.OpenSavingsAccount("Alan", 250m); + Account grace = b.OpenCurrentAccount("Grace", 500m, 200m); grace.Withdraw(50m); // Grace now 450 Assert.Equal(800m, b.TotalAssets); // 100 + 250 + 450 } @@ -57,8 +68,8 @@ public void TotalAssets_SumsEveryAccountsBalance() public void FindAccount_ReturnsTheMatchingAccount() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); - b.OpenAccount("Alan", 200m); + Account a = b.OpenSavingsAccount("Ada", 100m); + b.OpenCurrentAccount("Alan", 200m, 100m); Account? found = b.FindAccount(a.AccountNumber); Assert.NotNull(found); Assert.Same(a, found); @@ -68,7 +79,7 @@ public void FindAccount_ReturnsTheMatchingAccount() public void FindAccount_ReturnsNullWhenUnknown() { Bank b = new Bank("Acme"); - b.OpenAccount("Ada", 100m); + b.OpenSavingsAccount("Ada", 100m); Assert.Null(b.FindAccount("ACC-9999")); } @@ -76,8 +87,8 @@ public void FindAccount_ReturnsNullWhenUnknown() public void CloseAccount_RemovesTheAccountAndReturnsTrue() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); - b.OpenAccount("Alan", 200m); + Account a = b.OpenSavingsAccount("Ada", 100m); + b.OpenCurrentAccount("Alan", 200m, 100m); bool closed = b.CloseAccount(a.AccountNumber); Assert.True(closed); Assert.Equal(1, b.AccountCount); @@ -88,7 +99,7 @@ public void CloseAccount_RemovesTheAccountAndReturnsTrue() public void CloseAccount_ReturnsFalseWhenUnknown() { Bank b = new Bank("Acme"); - b.OpenAccount("Ada", 100m); + b.OpenSavingsAccount("Ada", 100m); Assert.False(b.CloseAccount("ACC-9999")); Assert.Equal(1, b.AccountCount); } @@ -97,7 +108,7 @@ public void CloseAccount_ReturnsFalseWhenUnknown() public void Accounts_ExposesReadOnlyView() { Bank b = new Bank("Acme"); - b.OpenAccount("Ada", 100m); + b.OpenSavingsAccount("Ada", 100m); Assert.IsAssignableFrom>(b.Accounts); } @@ -108,8 +119,8 @@ public void NextAccountNumber_IsInstanceScopedNotShared() // the counter is per-Bank, not global. Bank one = new Bank("One"); Bank two = new Bank("Two"); - Account a = one.OpenAccount("Ada", 100m); - Account c = two.OpenAccount("Alan", 200m); + Account a = one.OpenSavingsAccount("Ada", 100m); + Account c = two.OpenCurrentAccount("Alan", 200m, 50m); Assert.Equal("ACC-1000", a.AccountNumber); Assert.Equal("ACC-1000", c.AccountNumber); } @@ -118,18 +129,29 @@ public void NextAccountNumber_IsInstanceScopedNotShared() public void Transfer_TransfersFundsBetweenAccounts() { Bank bank = new Bank("Acme"); - Account a = bank.OpenAccount("Ada", 100m); - Account b = bank.OpenAccount("Alan", 200m); + Account a = bank.OpenSavingsAccount("Ada", 100m); + Account b = bank.OpenSavingsAccount("Alan", 200m); bank.Transfer(a.AccountNumber, b.AccountNumber, 50m); Assert.Equal(50m, a.Balance); Assert.Equal(250m, b.Balance); } + [Fact] + public void Transfer_AllowsCurrentAccountToUseItsOverdraftLimit() + { + Bank bank = new Bank("Acme"); + Account from = bank.OpenCurrentAccount("Ada", 100m, 50m); + Account to = bank.OpenSavingsAccount("Alan", 200m); + bank.Transfer(from.AccountNumber, to.AccountNumber, 125m); + Assert.Equal(-25m, from.Balance); + Assert.Equal(325m, to.Balance); + } + [Fact] public void Transfer_ThrowsWhenFromAccountNotFound() { Bank bank = new Bank("Acme"); - Account b = bank.OpenAccount("Alan", 200m); + Account b = bank.OpenSavingsAccount("Alan", 200m); Assert.Throws(() => bank.Transfer("ACC-9999", b.AccountNumber, 50m)); } @@ -137,7 +159,7 @@ public void Transfer_ThrowsWhenFromAccountNotFound() public void Transfer_ThrowsWhenToAccountNotFound() { Bank bank = new Bank("Acme"); - Account a = bank.OpenAccount("Ada", 100m); + Account a = bank.OpenSavingsAccount("Ada", 100m); Assert.Throws(() => bank.Transfer(a.AccountNumber, "ACC-9999", 50m)); } @@ -145,31 +167,31 @@ public void Transfer_ThrowsWhenToAccountNotFound() public void Transfer_ThrowsWhenInsufficientFunds() { Bank bank = new Bank("Acme"); - Account a = bank.OpenAccount("Ada", 100m); - Account b = bank.OpenAccount("Alan", 200m); + Account a = bank.OpenSavingsAccount("Ada", 100m); + Account b = bank.OpenSavingsAccount("Alan", 200m); Assert.Throws(() => bank.Transfer(a.AccountNumber, b.AccountNumber, 150m)); } [Fact] - public void ApplyInterest_CreditsInterestToAccountsWithPositiveBalance() + public void ApplyInterest_CreditsInterestToSavingsAccountsWithPositiveBalance() { Bank bank = new Bank("Acme"); - Account a = bank.OpenAccount("Ada", 100m); - Account b = bank.OpenAccount("Alan", 200m); + Account a = bank.OpenSavingsAccount("Ada", 100m); + Account b = bank.OpenSavingsAccount("Alan", 200m); bank.ApplyInterest(0.05m); Assert.Equal(105m, a.Balance); Assert.Equal(210m, b.Balance); } [Fact] - public void ApplyInterest_DoesNotCreditInterestToAccountsWithZeroOrNegativeBalance() + public void ApplyInterest_UsesPolymorphismForMixedAccountTypes() { Bank bank = new Bank("Acme"); - Account a = bank.OpenAccount("Ada", 0m); - Account b = bank.OpenAccount("Alan", 0m, 100m); - b.Withdraw(50m); + Account savings = bank.OpenSavingsAccount("Ada", 100m); + Account current = bank.OpenCurrentAccount("Alan", 0m, 100m); + current.Withdraw(50m); bank.ApplyInterest(0.05m); - Assert.Equal(0m, a.Balance); - Assert.Equal(-50m, b.Balance); + Assert.Equal(105m, savings.Balance); + Assert.Equal(-50m, current.Balance); } } diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs index 610d992..e08b2e4 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/Account.cs @@ -5,13 +5,12 @@ namespace BankApp; public class Account { private readonly Ledger _transactions; - private decimal AvailableBalance => Balance + OverdraftLimit; public string AccountNumber { get; } public string Holder { get; } - public decimal OverdraftLimit { get; } + public virtual decimal OverdraftLimit => 0m; - public Account(string accountNumber, string holder, decimal startingBalance, decimal overdraftLimit = 0m) + public Account(string accountNumber, string holder, decimal startingBalance) { if (startingBalance < 0) { @@ -21,7 +20,6 @@ public Account(string accountNumber, string holder, decimal startingBalance, dec AccountNumber = accountNumber; Holder = holder; _transactions = new Ledger(); - OverdraftLimit = overdraftLimit; if (startingBalance > 0) { _transactions.Add(new Transaction(TransactionType.Credit, startingBalance, TransactionCategory.Other, @@ -36,13 +34,16 @@ public decimal Balance decimal total = 0; foreach (Transaction transaction in _transactions) { - if (transaction.Type == TransactionType.Credit) + switch (transaction.Type) { - total += transaction.Amount; - } - else if (transaction.Type == TransactionType.Debit) - { - total -= transaction.Amount; + case TransactionType.Credit: + total += transaction.Amount; + break; + case TransactionType.Debit: + total -= transaction.Amount; + break; + default: + throw new ArgumentOutOfRangeException(nameof(transaction.Type)); } } @@ -54,6 +55,20 @@ public decimal Balance public IReadOnlyList Transactions => _transactions.Entries; + protected void RecordCredit(decimal amount, DateTime timestamp, TransactionCategory category, string description) + { + decimal newBalance = Balance + amount; + string desc = description + $" (New Balance: {newBalance:N2})"; + _transactions.Add(new Transaction(TransactionType.Credit, amount, category, timestamp, desc)); + } + + protected void RecordDebit(decimal amount, DateTime timestamp, TransactionCategory category, string description) + { + decimal newBalance = Balance - amount; + string desc = description + $" (New Balance: {newBalance:N2})"; + _transactions.Add(new Transaction(TransactionType.Debit, amount, category, timestamp, desc)); + } + public void Deposit(decimal amount, TransactionCategory category = TransactionCategory.Other, string description = "Deposit") { @@ -68,9 +83,7 @@ public void Deposit(decimal amount, DateTime timestamp, TransactionCategory cate throw new ArgumentException("Amount must be greater than 0"); } - decimal newBalance = Balance + amount; - string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Credit, amount, category, timestamp, desc)); + RecordCredit(amount, timestamp, category, description); } public void Withdraw(decimal amount, TransactionCategory category = TransactionCategory.Other, @@ -79,7 +92,8 @@ public void Withdraw(decimal amount, TransactionCategory category = TransactionC Withdraw(amount, DateTime.UtcNow, category, description); } - public void Withdraw(decimal amount, DateTime timestamp, TransactionCategory category = TransactionCategory.Other, + public virtual void Withdraw(decimal amount, DateTime timestamp, + TransactionCategory category = TransactionCategory.Other, string description = "Withdrawal") { if (amount <= 0) @@ -87,14 +101,16 @@ public void Withdraw(decimal amount, DateTime timestamp, TransactionCategory cat throw new ArgumentException("Amount must be greater than 0"); } - if (amount > AvailableBalance) + if (amount > Balance) { - throw new InsufficientFundsException(amount, AvailableBalance); + throw new InsufficientFundsException(amount, Balance); } - decimal newBalance = Balance - amount; - string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Debit, amount, category, timestamp, desc)); + RecordDebit(amount, timestamp, category, description); + } + + public virtual void ApplyInterest(decimal rate) + { } private string BuildStatement(IEnumerable includedTransactions) diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index ff2535e..1a297b5 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -23,11 +23,20 @@ public decimal TotalAssets public IReadOnlyList Accounts => _accounts.AsReadOnly(); - public Account OpenAccount(string holder, decimal startingBalance, decimal overdraftLimit = 0m) + public SavingsAccount OpenSavingsAccount(string holder, decimal startingBalance) { string accountNumber = $"ACC-{_nextAccountNumber}"; _nextAccountNumber++; - Account account = new Account(accountNumber, holder, startingBalance, overdraftLimit); + SavingsAccount account = new SavingsAccount(accountNumber, holder, startingBalance); + _accounts.Add(account); + return account; + } + + public CurrentAccount OpenCurrentAccount(string holder, decimal startingBalance, decimal overdraftLimit) + { + string accountNumber = $"ACC-{_nextAccountNumber}"; + _nextAccountNumber++; + CurrentAccount account = new CurrentAccount(accountNumber, holder, startingBalance, overdraftLimit); _accounts.Add(account); return account; } @@ -63,21 +72,15 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a throw new InvalidOperationException("To account not found"); } - if (fromAccount.Balance < amount) - { - throw new InsufficientFundsException(amount, fromAccount.Balance, - $"Insufficient funds in {fromAccountNumber}"); - } - fromAccount.Withdraw(amount, TransactionCategory.Transfer, $"Transfer to {toAccountNumber}"); toAccount.Deposit(amount, TransactionCategory.Transfer, $"Transfer from {fromAccountNumber}"); } public void ApplyInterest(decimal rate) { - foreach (Account account in _accounts.Where(a => a.Balance > 0)) + foreach (Account account in _accounts) { - account.Deposit(account.Balance * rate, TransactionCategory.Interest, $"Interest {rate:P2}"); + account.ApplyInterest(rate); } } } diff --git a/projects/bank/Bank/CurrentAccount.cs b/projects/bank/Bank/CurrentAccount.cs new file mode 100644 index 0000000..37947f6 --- /dev/null +++ b/projects/bank/Bank/CurrentAccount.cs @@ -0,0 +1,35 @@ +namespace BankApp; + +public class CurrentAccount : Account +{ + public override decimal OverdraftLimit { get; } + + public CurrentAccount(string accountNumber, string holder, decimal startingBalance, decimal overdraftLimit) + : base(accountNumber, holder, startingBalance) + { + if (overdraftLimit < 0) + { + throw new ArgumentException("Overdraft limit must be greater than or equal to 0"); + } + + OverdraftLimit = overdraftLimit; + } + + public override void Withdraw(decimal amount, DateTime timestamp, + TransactionCategory category = TransactionCategory.Other, + string description = "Withdrawal") + { + if (amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + decimal availableBalance = Balance + OverdraftLimit; + if (amount > availableBalance) + { + throw new InsufficientFundsException(amount, availableBalance); + } + + RecordDebit(amount, timestamp, category, description); + } +} diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 513278f..650718f 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -1,9 +1,9 @@ using BankApp; Bank bank = new Bank("Acme Savings"); -Account a = bank.OpenAccount("Ada Lovelace", 200m); -Account b = bank.OpenAccount("Alan Turing", 200m); -Account c = bank.OpenAccount("Bob Smith", 0m, 100m); +Account a = bank.OpenSavingsAccount("Ada Lovelace", 200m); +Account b = bank.OpenSavingsAccount("Alan Turing", 200m); +Account c = bank.OpenCurrentAccount("Bob Smith", 0m, 100m); c.Withdraw(50m); a.Deposit(50m); a.Withdraw(20m); diff --git a/projects/bank/Bank/SavingsAccount.cs b/projects/bank/Bank/SavingsAccount.cs new file mode 100644 index 0000000..5210fce --- /dev/null +++ b/projects/bank/Bank/SavingsAccount.cs @@ -0,0 +1,24 @@ +namespace BankApp; + +public class SavingsAccount : Account +{ + public SavingsAccount(string accountNumber, string holder, decimal startingBalance) + : base(accountNumber, holder, startingBalance) + { + } + + public override void ApplyInterest(decimal rate) + { + if (rate <= 0) + { + throw new ArgumentException("Rate must be greater than 0"); + } + + if (Balance <= 0) + { + throw new InvalidOperationException("Balance must be greater than 0"); + } + + RecordCredit(Balance * rate, DateTime.UtcNow, TransactionCategory.Interest, $"Interest {rate:P2}"); + } +} From 73fb6818e6f38607c4151653659b09384049d4b7 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:29:13 +0200 Subject: [PATCH 18/27] folders + GlobalUsings --- projects/bank/Bank.Tests/Bank.Tests.csproj | 1 + projects/bank/Bank/GlobalUsings.cs | 6 ++++++ projects/bank/Bank/Program.cs | 2 -- projects/bank/Bank/{ => account}/Account.cs | 4 +--- projects/bank/Bank/{ => account}/CurrentAccount.cs | 2 +- projects/bank/Bank/{ => account}/Ledger.cs | 2 +- projects/bank/Bank/{ => account}/SavingsAccount.cs | 2 +- projects/bank/Bank/{ => transaction}/Transaction.cs | 2 +- projects/bank/Bank/{ => transaction}/TransactionCategory.cs | 2 +- projects/bank/Bank/{ => transaction}/TransactionType.cs | 2 +- 10 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 projects/bank/Bank/GlobalUsings.cs rename projects/bank/Bank/{ => account}/Account.cs (99%) rename projects/bank/Bank/{ => account}/CurrentAccount.cs (97%) rename projects/bank/Bank/{ => account}/Ledger.cs (94%) rename projects/bank/Bank/{ => account}/SavingsAccount.cs (95%) rename projects/bank/Bank/{ => transaction}/Transaction.cs (96%) rename projects/bank/Bank/{ => transaction}/TransactionCategory.cs (79%) rename projects/bank/Bank/{ => transaction}/TransactionType.cs (85%) diff --git a/projects/bank/Bank.Tests/Bank.Tests.csproj b/projects/bank/Bank.Tests/Bank.Tests.csproj index f895198..7569ec9 100644 --- a/projects/bank/Bank.Tests/Bank.Tests.csproj +++ b/projects/bank/Bank.Tests/Bank.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/projects/bank/Bank/GlobalUsings.cs b/projects/bank/Bank/GlobalUsings.cs new file mode 100644 index 0000000..322f774 --- /dev/null +++ b/projects/bank/Bank/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using System.Text; + +// App +global using BankApp; +global using BankApp.account; +global using BankApp.transaction; \ No newline at end of file diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index 650718f..a07bb49 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -1,5 +1,3 @@ -using BankApp; - Bank bank = new Bank("Acme Savings"); Account a = bank.OpenSavingsAccount("Ada Lovelace", 200m); Account b = bank.OpenSavingsAccount("Alan Turing", 200m); diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/account/Account.cs similarity index 99% rename from projects/bank/Bank/Account.cs rename to projects/bank/Bank/account/Account.cs index e08b2e4..8795ba1 100644 --- a/projects/bank/Bank/Account.cs +++ b/projects/bank/Bank/account/Account.cs @@ -1,6 +1,4 @@ -using System.Text; - -namespace BankApp; +namespace BankApp.account; public class Account { diff --git a/projects/bank/Bank/CurrentAccount.cs b/projects/bank/Bank/account/CurrentAccount.cs similarity index 97% rename from projects/bank/Bank/CurrentAccount.cs rename to projects/bank/Bank/account/CurrentAccount.cs index 37947f6..5024189 100644 --- a/projects/bank/Bank/CurrentAccount.cs +++ b/projects/bank/Bank/account/CurrentAccount.cs @@ -1,4 +1,4 @@ -namespace BankApp; +namespace BankApp.account; public class CurrentAccount : Account { diff --git a/projects/bank/Bank/Ledger.cs b/projects/bank/Bank/account/Ledger.cs similarity index 94% rename from projects/bank/Bank/Ledger.cs rename to projects/bank/Bank/account/Ledger.cs index 9c47032..c61a656 100644 --- a/projects/bank/Bank/Ledger.cs +++ b/projects/bank/Bank/account/Ledger.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace BankApp; +namespace BankApp.account; public class Ledger : IEnumerable { diff --git a/projects/bank/Bank/SavingsAccount.cs b/projects/bank/Bank/account/SavingsAccount.cs similarity index 95% rename from projects/bank/Bank/SavingsAccount.cs rename to projects/bank/Bank/account/SavingsAccount.cs index 5210fce..06b320c 100644 --- a/projects/bank/Bank/SavingsAccount.cs +++ b/projects/bank/Bank/account/SavingsAccount.cs @@ -1,4 +1,4 @@ -namespace BankApp; +namespace BankApp.account; public class SavingsAccount : Account { diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/transaction/Transaction.cs similarity index 96% rename from projects/bank/Bank/Transaction.cs rename to projects/bank/Bank/transaction/Transaction.cs index 133b924..1c3dc9c 100644 --- a/projects/bank/Bank/Transaction.cs +++ b/projects/bank/Bank/transaction/Transaction.cs @@ -1,4 +1,4 @@ -namespace BankApp; +namespace BankApp.transaction; public struct Transaction { diff --git a/projects/bank/Bank/TransactionCategory.cs b/projects/bank/Bank/transaction/TransactionCategory.cs similarity index 79% rename from projects/bank/Bank/TransactionCategory.cs rename to projects/bank/Bank/transaction/TransactionCategory.cs index 1da295a..e711650 100644 --- a/projects/bank/Bank/TransactionCategory.cs +++ b/projects/bank/Bank/transaction/TransactionCategory.cs @@ -1,4 +1,4 @@ -namespace BankApp; +namespace BankApp.transaction; public enum TransactionCategory { diff --git a/projects/bank/Bank/TransactionType.cs b/projects/bank/Bank/transaction/TransactionType.cs similarity index 85% rename from projects/bank/Bank/TransactionType.cs rename to projects/bank/Bank/transaction/TransactionType.cs index 84e0fd7..5ad1d4e 100644 --- a/projects/bank/Bank/TransactionType.cs +++ b/projects/bank/Bank/transaction/TransactionType.cs @@ -1,4 +1,4 @@ -namespace BankApp; +namespace BankApp.transaction; public enum TransactionType { From 3eabbddd4fbc13e199a2a9612eb21168e810ed3b Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:16:52 +0200 Subject: [PATCH 19/27] sandbox --- projects/oop_sandbox/OopWarmup/Canvas.cs | 11 ++++++++++- projects/oop_sandbox/OopWarmup/Triangle.cs | 17 +++++++++-------- projects/oop_sandbox/OopWarmup/Vector2D.cs | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/projects/oop_sandbox/OopWarmup/Canvas.cs b/projects/oop_sandbox/OopWarmup/Canvas.cs index d007e7f..19aa21c 100644 --- a/projects/oop_sandbox/OopWarmup/Canvas.cs +++ b/projects/oop_sandbox/OopWarmup/Canvas.cs @@ -31,6 +31,15 @@ public void DescribeAll() // that was added, not a new or copied shape. public Shape? FindLargest() { - throw new NotImplementedException("TODO: iterate shapes, return the one with max Area; null if empty"); + Shape? largest = null; + foreach (Shape s in shapes) + { + if (largest == null || s.Area > largest.Area) + { + largest = s; + } + } + + return largest; } } diff --git a/projects/oop_sandbox/OopWarmup/Triangle.cs b/projects/oop_sandbox/OopWarmup/Triangle.cs index 8304708..63234f9 100644 --- a/projects/oop_sandbox/OopWarmup/Triangle.cs +++ b/projects/oop_sandbox/OopWarmup/Triangle.cs @@ -20,14 +20,15 @@ public Triangle(Point a, Point b, Point c) public override double Area { - // TODO: use Heron's formula. You'll need the side lengths first — - // Point.DistanceTo is already implemented. - get { throw new NotImplementedException("TODO: Heron's formula — see README link"); } + get + { + double ab = A.DistanceTo(B); + double bc = B.DistanceTo(C); + double ca = C.DistanceTo(A); + double s = (ab + bc + ca) / 2; + return Math.Sqrt(s * (s - ab) * (s - bc) * (s - ca)); + } } - public override double Perimeter - { - // TODO: sum of the three side lengths. - get { throw new NotImplementedException("TODO: sum of A-B, B-C, C-A distances"); } - } + public override double Perimeter => A.DistanceTo(B) + B.DistanceTo(C) + C.DistanceTo(A); } diff --git a/projects/oop_sandbox/OopWarmup/Vector2D.cs b/projects/oop_sandbox/OopWarmup/Vector2D.cs index 8047e3b..d0b0f99 100644 --- a/projects/oop_sandbox/OopWarmup/Vector2D.cs +++ b/projects/oop_sandbox/OopWarmup/Vector2D.cs @@ -34,7 +34,7 @@ public void ScaleBy(double factor) // See the README for the exercise brief and a Wikipedia link. public double DotProduct(Vector2D other) { - throw new NotImplementedException("TODO: return X * other.X + Y * other.Y"); + return X * other.X + Y * other.Y; } // EXERCISE 2: ToString override @@ -43,6 +43,6 @@ public double DotProduct(Vector2D other) // Every type in C# inherits from object — see the README link. public override string ToString() { - throw new NotImplementedException("TODO: return $\"[{X}, {Y}]\""); + return $"[{X}, {Y}]"; } } From 8ab4908ef5a1b9bb0c375ccdda5f2e130826b0ab Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:12:51 +0200 Subject: [PATCH 20/27] filesystem - largestFile --- csharp.md | 2 ++ .../FileSystem.Tests/DirectoryNodeTests.cs | 30 +++++++++++++++++++ .../FileSystem.Tests/FileNodeTests.cs | 7 +++++ .../filesystem/FileSystem/DirectoryNode.cs | 23 +++++++++++++- projects/filesystem/FileSystem/FSNode.cs | 2 ++ projects/filesystem/FileSystem/FileNode.cs | 5 ++++ 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/csharp.md b/csharp.md index 0310b5d..61f4a52 100644 --- a/csharp.md +++ b/csharp.md @@ -75,6 +75,8 @@ String formatting: - use structs for small data, e.g. coordinates, money, date ranges - use classes when data gets more complex than that - polymorphism: overriding parent methods +- abstract class: cannot create object of class, must be inherited (incomplete implementation) +- interface: describing functionality, e.g. ISearchable, IEnumerable **Enums** diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index d0d5afe..cc8c967 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -123,4 +123,34 @@ public void Children_ExposesAddedInOrder() Assert.Equal(new FSNode[] { a, b, c }, d.Children); } + + [Fact] + public void LargestFile_ReturnsLargestFile() + { + DirectoryNode d = new DirectoryNode("d"); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.txt", 2); + FileNode c = new FileNode("c.txt", 3); + d.Add(a); + d.Add(b); + d.Add(c); + + Assert.Equal(c, d.LargestFile()); + } + + [Fact] + public void LargestFile_ReturnsLargestFileInNestedDirectory() + { + DirectoryNode d = new DirectoryNode("d"); + DirectoryNode d2 = new DirectoryNode("d2"); + d.Add(d2); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.txt", 2); + FileNode c = new FileNode("c.txt", 3); + d2.Add(a); + d2.Add(b); + d2.Add(c); + + Assert.Equal(c, d.LargestFile()); + } } diff --git a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs index 07de079..0cf32b3 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -73,4 +73,11 @@ public void FindByName_IsCaseSensitive() FileNode f = new FileNode("README.md", 100); Assert.Null(f.FindByName("readme.md")); } + + [Fact] + public void LargestFile_ReturnsSelf() + { + FileNode f = new FileNode("readme.md", 100); + Assert.Same(f, f.LargestFile()); + } } diff --git a/projects/filesystem/FileSystem/DirectoryNode.cs b/projects/filesystem/FileSystem/DirectoryNode.cs index e59ed7d..e81fb66 100644 --- a/projects/filesystem/FileSystem/DirectoryNode.cs +++ b/projects/filesystem/FileSystem/DirectoryNode.cs @@ -20,7 +20,9 @@ public class DirectoryNode : FSNode, ISearchable { private readonly List children = new(); - public DirectoryNode(string name) : base(name) { } + public DirectoryNode(string name) : base(name) + { + } // Expose children as read-only. Callers can enumerate them, but // cannot Add / Remove / Clear — the only mutation path is Add(). @@ -74,6 +76,25 @@ public override void Print(int indent = 0) if (hit != null) return hit; } } + return null; } + + public override FileNode? LargestFile() + { + FileNode? largest = null; + foreach (FSNode child in children) + { + if (child is FileNode file && (largest == null || file.Size() > largest.Size())) + { + largest = file; + } + else if (child is DirectoryNode directory) + { + largest = directory.LargestFile(); + } + } + + return largest; + } } diff --git a/projects/filesystem/FileSystem/FSNode.cs b/projects/filesystem/FileSystem/FSNode.cs index 8bd6817..a1e5391 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -40,4 +40,6 @@ protected FSNode(string name) // `indent` controls nesting — each level adds two spaces of leading // whitespace, so DirectoryNode can call child.Print(indent + 1). public abstract void Print(int indent = 0); + + public abstract FileNode? LargestFile(); } diff --git a/projects/filesystem/FileSystem/FileNode.cs b/projects/filesystem/FileSystem/FileNode.cs index b83d19d..3e23663 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -44,4 +44,9 @@ public override void Print(int indent = 0) { return Name == name ? this : null; } + + public override FileNode LargestFile() + { + return this; + } } From 71ce66c7a7d14fe592644c1bcac9ce8fd6e57b0a Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:26:13 +0200 Subject: [PATCH 21/27] FilterByExtension --- .../FileSystem.Tests/DirectoryNodeTests.cs | 16 ++++++++++++++++ .../filesystem/FileSystem.Tests/FileNodeTests.cs | 7 +++++++ projects/filesystem/FileSystem/DirectoryNode.cs | 11 +++++++++++ projects/filesystem/FileSystem/FSNode.cs | 2 ++ projects/filesystem/FileSystem/FileNode.cs | 7 +++++++ 5 files changed, 43 insertions(+) diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index cc8c967..1b687de 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -153,4 +153,20 @@ public void LargestFile_ReturnsLargestFileInNestedDirectory() Assert.Equal(c, d.LargestFile()); } + + [Fact] + public void FilterByExtension_ReturnsFilesWithMatchingExtension() + { + DirectoryNode d = new DirectoryNode("d"); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d.Add(a); + d.Add(b); + d.Add(c); + + Assert.Equal([b], d.FilterByExtension(".md")); + Assert.Equal([a, c], d.FilterByExtension(".txt")); + Assert.Equal([], d.FilterByExtension(".cs")); + } } diff --git a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs index 0cf32b3..a3aff61 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -80,4 +80,11 @@ public void LargestFile_ReturnsSelf() FileNode f = new FileNode("readme.md", 100); Assert.Same(f, f.LargestFile()); } + + [Fact] + public void FilterByExtension_ReturnsSelfWhenMatching() + { + FileNode f = new FileNode("readme.md", 100); + Assert.Equal([f], f.FilterByExtension(".md")); + } } diff --git a/projects/filesystem/FileSystem/DirectoryNode.cs b/projects/filesystem/FileSystem/DirectoryNode.cs index e81fb66..6e80356 100644 --- a/projects/filesystem/FileSystem/DirectoryNode.cs +++ b/projects/filesystem/FileSystem/DirectoryNode.cs @@ -97,4 +97,15 @@ public override void Print(int indent = 0) return largest; } + + public override List FilterByExtension(string ext) + { + List result = []; + foreach (FSNode child in children) + { + result.AddRange(child.FilterByExtension(ext)); + } + + return result; + } } diff --git a/projects/filesystem/FileSystem/FSNode.cs b/projects/filesystem/FileSystem/FSNode.cs index a1e5391..15e931f 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -42,4 +42,6 @@ protected FSNode(string name) public abstract void Print(int indent = 0); public abstract FileNode? LargestFile(); + + public abstract List FilterByExtension(string ext); } diff --git a/projects/filesystem/FileSystem/FileNode.cs b/projects/filesystem/FileSystem/FileNode.cs index 3e23663..7e6503e 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -49,4 +49,11 @@ public override FileNode LargestFile() { return this; } + + public override List FilterByExtension(string ext) + { + return Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase) + ? [this] + : new List(); + } } From 1be027e4dc572e6fb675c3f63629fc4865cae4c2 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:49:11 +0200 Subject: [PATCH 22/27] CountByExtension --- csharp.md | 1 + .../FileSystem.Tests/DirectoryNodeTests.cs | 47 +++++++++++++++++++ .../FileSystem.Tests/FileNodeTests.cs | 21 +++++++++ .../filesystem/FileSystem/DirectoryNode.cs | 14 ++++++ projects/filesystem/FileSystem/FSNode.cs | 2 + projects/filesystem/FileSystem/FileNode.cs | 5 ++ 6 files changed, 90 insertions(+) diff --git a/csharp.md b/csharp.md index 61f4a52..1a109bc 100644 --- a/csharp.md +++ b/csharp.md @@ -77,6 +77,7 @@ String formatting: - polymorphism: overriding parent methods - abstract class: cannot create object of class, must be inherited (incomplete implementation) - interface: describing functionality, e.g. ISearchable, IEnumerable +- Dictionary = Map **Enums** diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index 1b687de..88e9e89 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -169,4 +169,51 @@ public void FilterByExtension_ReturnsFilesWithMatchingExtension() Assert.Equal([a, c], d.FilterByExtension(".txt")); Assert.Equal([], d.FilterByExtension(".cs")); } + + [Fact] + public void FilterByExtension_ReturnsFilesWithMatchingExtensionInNestedDirectories() + { + DirectoryNode d = new DirectoryNode("d"); + DirectoryNode d2 = new DirectoryNode("d2"); + d.Add(d2); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d2.Add(a); + d2.Add(b); + d2.Add(c); + + Assert.Equal([a, c], d.FilterByExtension(".txt")); + Assert.Equal([b], d.FilterByExtension(".md")); + } + + [Fact] + public void CountByExtension_ReturnsCountsForExtensions() + { + DirectoryNode d = new DirectoryNode("d"); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d.Add(a); + d.Add(b); + d.Add(c); + + Assert.Equal(new Dictionary { { ".txt", 2 }, { ".md", 1 } }, d.CountByExtension()); + } + + [Fact] + public void CountByExtension_ReturnsCountsForExtensionsInNestedDirectories() + { + DirectoryNode d = new DirectoryNode("d"); + DirectoryNode d2 = new DirectoryNode("d2"); + d.Add(d2); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d2.Add(a); + d2.Add(b); + d2.Add(c); + + Assert.Equal(new Dictionary { { ".txt", 2 }, { ".md", 1 } }, d.CountByExtension()); + } } diff --git a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs index a3aff61..0319cba 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -87,4 +87,25 @@ public void FilterByExtension_ReturnsSelfWhenMatching() FileNode f = new FileNode("readme.md", 100); Assert.Equal([f], f.FilterByExtension(".md")); } + + [Fact] + public void FilterByExtension_IsCaseInsensitive() + { + FileNode f = new FileNode("README.MD", 100); + Assert.Equal([f], f.FilterByExtension(".md")); + } + + [Fact] + public void CountByExtension_ReturnsOneForExtension() + { + FileNode f = new FileNode("readme.md", 100); + Assert.Equal(new Dictionary { { ".md", 1 } }, f.CountByExtension()); + } + + [Fact] + public void CountByExtension_IsCaseInsensitive() + { + FileNode f = new FileNode("README.MD", 100); + Assert.Equal(new Dictionary { { ".md", 1 } }, f.CountByExtension()); + } } diff --git a/projects/filesystem/FileSystem/DirectoryNode.cs b/projects/filesystem/FileSystem/DirectoryNode.cs index 6e80356..fcb3123 100644 --- a/projects/filesystem/FileSystem/DirectoryNode.cs +++ b/projects/filesystem/FileSystem/DirectoryNode.cs @@ -108,4 +108,18 @@ public override List FilterByExtension(string ext) return result; } + + public override Dictionary CountByExtension() + { + Dictionary result = []; + foreach (FSNode child in children) + { + foreach (var (key, value) in child.CountByExtension()) + { + result[key] = result.TryGetValue(key, out int count) ? count + value : value; + } + } + + return result; + } } diff --git a/projects/filesystem/FileSystem/FSNode.cs b/projects/filesystem/FileSystem/FSNode.cs index 15e931f..1f407cd 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -44,4 +44,6 @@ protected FSNode(string name) public abstract FileNode? LargestFile(); public abstract List FilterByExtension(string ext); + + public abstract Dictionary CountByExtension(); } diff --git a/projects/filesystem/FileSystem/FileNode.cs b/projects/filesystem/FileSystem/FileNode.cs index 7e6503e..6e557e0 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -56,4 +56,9 @@ public override List FilterByExtension(string ext) ? [this] : new List(); } + + public override Dictionary CountByExtension() + { + return new Dictionary { { Path.GetExtension(Name).ToLowerInvariant(), 1 } }; + } } From 43c19e1ae6ea382d8a855d7a550440063bbccca7 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:04:36 +0200 Subject: [PATCH 23/27] Depth --- .../FileSystem.Tests/DirectoryNodeTests.cs | 34 +++++++++++++++++++ .../FileSystem.Tests/FileNodeTests.cs | 7 ++++ .../filesystem/FileSystem/DirectoryNode.cs | 6 ++++ projects/filesystem/FileSystem/FSNode.cs | 2 ++ projects/filesystem/FileSystem/FileNode.cs | 5 +++ 5 files changed, 54 insertions(+) diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index 88e9e89..e180646 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -216,4 +216,38 @@ public void CountByExtension_ReturnsCountsForExtensionsInNestedDirectories() Assert.Equal(new Dictionary { { ".txt", 2 }, { ".md", 1 } }, d.CountByExtension()); } + + [Fact] + public void Depth_ReturnsDepthOfDirectory() + { + DirectoryNode d = new DirectoryNode("d"); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d.Add(a); + d.Add(b); + d.Add(c); + + Assert.Equal(1, d.Depth()); + } + + [Fact] + public void Depth_ReturnsDepthOfNestedDirectories() + { + DirectoryNode d = new DirectoryNode("d"); + DirectoryNode d2 = new DirectoryNode("d2"); + DirectoryNode d3 = new DirectoryNode("d3"); + d.Add(d2); + d2.Add(d3); + Assert.Equal(2, d.Depth()); + Assert.Equal(1, d2.Depth()); + Assert.Equal(0, d3.Depth()); + } + + [Fact] + public void Depth_ReturnsDepthOfEmptyDirectory() + { + DirectoryNode d = new DirectoryNode("d"); + Assert.Equal(0, d.Depth()); + } } diff --git a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs index 0319cba..db5879f 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -108,4 +108,11 @@ public void CountByExtension_IsCaseInsensitive() FileNode f = new FileNode("README.MD", 100); Assert.Equal(new Dictionary { { ".md", 1 } }, f.CountByExtension()); } + + [Fact] + public void Depth_ReturnsZero() + { + FileNode f = new FileNode("readme.md", 100); + Assert.Equal(0, f.Depth()); + } } diff --git a/projects/filesystem/FileSystem/DirectoryNode.cs b/projects/filesystem/FileSystem/DirectoryNode.cs index fcb3123..f2cdd85 100644 --- a/projects/filesystem/FileSystem/DirectoryNode.cs +++ b/projects/filesystem/FileSystem/DirectoryNode.cs @@ -122,4 +122,10 @@ public override Dictionary CountByExtension() return result; } + + public override int Depth() + { + if (children.Count == 0) return 0; + return 1 + children.Max(child => child.Depth()); + } } diff --git a/projects/filesystem/FileSystem/FSNode.cs b/projects/filesystem/FileSystem/FSNode.cs index 1f407cd..f44394f 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -46,4 +46,6 @@ protected FSNode(string name) public abstract List FilterByExtension(string ext); public abstract Dictionary CountByExtension(); + + public abstract int Depth(); } diff --git a/projects/filesystem/FileSystem/FileNode.cs b/projects/filesystem/FileSystem/FileNode.cs index 6e557e0..0ce0884 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -61,4 +61,9 @@ public override Dictionary CountByExtension() { return new Dictionary { { Path.GetExtension(Name).ToLowerInvariant(), 1 } }; } + + public override int Depth() + { + return 0; + } } From aeb85ba58d3efa827d1d116d1d4c90225d2a7e1d Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:36:44 +0200 Subject: [PATCH 24/27] PrettyPrint --- .../FileSystem.Tests/DirectoryNodeTests.cs | 36 +++++++++++++++++++ .../FileSystem.Tests/FileNodeTests.cs | 10 ++++++ .../filesystem/FileSystem/DirectoryNode.cs | 22 ++++++++++++ projects/filesystem/FileSystem/FSNode.cs | 2 ++ projects/filesystem/FileSystem/FileNode.cs | 13 +++++++ projects/filesystem/FileSystem/Program.cs | 4 +++ 6 files changed, 87 insertions(+) diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index e180646..042f792 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -250,4 +250,40 @@ public void Depth_ReturnsDepthOfEmptyDirectory() DirectoryNode d = new DirectoryNode("d"); Assert.Equal(0, d.Depth()); } + + [Fact] + public void PrettyPrint_PrintsDirectory() + { + DirectoryNode d = new DirectoryNode("d"); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d.Add(a); + d.Add(b); + d.Add(c); + + StringWriter sw = new StringWriter(); + Console.SetOut(sw); + d.PrettyPrint(); + Assert.Equal("d/\r\n├── a.txt\r\n├── b.md\r\n└── c.txt", sw.ToString().Trim()); + } + + [Fact] + public void PrettyPrint_PrintsNestedDirectory() + { + DirectoryNode d = new DirectoryNode("d"); + DirectoryNode d2 = new DirectoryNode("d2"); + d.Add(d2); + FileNode a = new FileNode("a.txt", 1); + FileNode b = new FileNode("b.md", 2); + FileNode c = new FileNode("c.txt", 3); + d2.Add(a); + d2.Add(b); + d2.Add(c); + + StringWriter sw = new StringWriter(); + Console.SetOut(sw); + d.PrettyPrint(); + Assert.Equal("d/\r\n└── d2/\r\n\t├── a.txt\r\n\t├── b.md\r\n\t└── c.txt", sw.ToString().Trim()); + } } diff --git a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs index db5879f..6f7cbaf 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -115,4 +115,14 @@ public void Depth_ReturnsZero() FileNode f = new FileNode("readme.md", 100); Assert.Equal(0, f.Depth()); } + + [Fact] + public void PrettyPrint_PrintsFile() + { + FileNode f = new FileNode("readme.md", 100); + StringWriter sw = new StringWriter(); + Console.SetOut(sw); + f.PrettyPrint(); + Assert.Equal("readme.md", sw.ToString().Trim()); + } } diff --git a/projects/filesystem/FileSystem/DirectoryNode.cs b/projects/filesystem/FileSystem/DirectoryNode.cs index f2cdd85..b93f916 100644 --- a/projects/filesystem/FileSystem/DirectoryNode.cs +++ b/projects/filesystem/FileSystem/DirectoryNode.cs @@ -128,4 +128,26 @@ public override int Depth() if (children.Count == 0) return 0; return 1 + children.Max(child => child.Depth()); } + + public override void PrettyPrint(string? prefix = null, bool isLast = true) + { + bool isRoot = prefix is null; + + if (isRoot) + { + Console.WriteLine(Name + "/"); + } + else + { + Console.WriteLine(prefix + (isLast ? "└── " : "├── ") + Name + "/"); + } + + string childPrefix = isRoot ? string.Empty : prefix + (isLast ? "\t" : "│ "); + + for (int i = 0; i < children.Count; i++) + { + bool childIsLast = i == children.Count - 1; + children[i].PrettyPrint(childPrefix, childIsLast); + } + } } diff --git a/projects/filesystem/FileSystem/FSNode.cs b/projects/filesystem/FileSystem/FSNode.cs index f44394f..9705c0a 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -48,4 +48,6 @@ protected FSNode(string name) public abstract Dictionary CountByExtension(); public abstract int Depth(); + + public abstract void PrettyPrint(string? prefix = null, bool isLast = true); } diff --git a/projects/filesystem/FileSystem/FileNode.cs b/projects/filesystem/FileSystem/FileNode.cs index 0ce0884..a3a710a 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -66,4 +66,17 @@ public override int Depth() { return 0; } + + public override void PrettyPrint(string? prefix = null, bool isLast = true) + { + bool isRoot = prefix is null; + + if (isRoot) + { + Console.WriteLine(Name); + return; + } + + Console.WriteLine(prefix + (isLast ? "└── " : "├── ") + Name); + } } diff --git a/projects/filesystem/FileSystem/Program.cs b/projects/filesystem/FileSystem/Program.cs index 2cdf804..66284b3 100644 --- a/projects/filesystem/FileSystem/Program.cs +++ b/projects/filesystem/FileSystem/Program.cs @@ -74,3 +74,7 @@ // • Build a much deeper tree and check Size / FileCount stay correct. // • After implementing the pretty-print exercise, swap the call to // root.Print() above for root.PrettyPrint() to see the difference. + +Console.WriteLine("=== PRETTY PRINT ==="); +root.PrettyPrint(); + From c53c687d921f2e3774d695f95db42774bf859f3f Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:36:05 +0200 Subject: [PATCH 25/27] TransactionProps & Request --- projects/bank/Bank.Tests/AccountTests.cs | 54 ++++++------ projects/bank/Bank.Tests/BankTests.cs | 4 +- projects/bank/Bank.Tests/TransactionTests.cs | 15 +++- projects/bank/Bank/Bank.cs | 10 ++- projects/bank/Bank/Program.cs | 8 +- projects/bank/Bank/account/Account.cs | 87 ++++++++++++------- projects/bank/Bank/account/CurrentAccount.cs | 16 ++-- projects/bank/Bank/account/SavingsAccount.cs | 8 +- projects/bank/Bank/transaction/Transaction.cs | 20 ++--- .../bank/Bank/transaction/TransactionProps.cs | 9 ++ .../Bank/transaction/TransactionRequest.cs | 8 ++ 11 files changed, 145 insertions(+), 94 deletions(-) create mode 100644 projects/bank/Bank/transaction/TransactionProps.cs create mode 100644 projects/bank/Bank/transaction/TransactionRequest.cs diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index f88af45..13d5a35 100644 --- a/projects/bank/Bank.Tests/AccountTests.cs +++ b/projects/bank/Bank.Tests/AccountTests.cs @@ -49,9 +49,9 @@ public void Balance_MatchesStartingDeposit() public void Balance_ReflectsCreditsAndDebits() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); - a.Withdraw(30m); - a.Deposit(10m); + a.Deposit(new TransactionRequest { Amount = 50m }); + a.Withdraw(new TransactionRequest { Amount = 30m }); + a.Deposit(new TransactionRequest { Amount = 10m }); Assert.Equal(130m, a.Balance); } @@ -61,7 +61,7 @@ public void Balance_ReflectsCreditsAndDebits() public void Deposit_AddsCreditTransaction() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); + a.Deposit(new TransactionRequest { Amount = 50m }); Assert.Equal(2, a.TransactionCount); Assert.Equal(TransactionType.Credit, a.Transactions[1].Type); Assert.Equal(50m, a.Transactions[1].Amount); @@ -74,7 +74,7 @@ public void Deposit_AddsCreditTransaction() public void Deposit_ThrowsOnNonPositiveAmount(decimal badAmount) { Account a = new Account("ACC-1000", "Ada", 100m); - Assert.Throws(() => a.Deposit(badAmount)); + Assert.Throws(() => a.Deposit(new TransactionRequest { Amount = badAmount })); } // ── Withdraw ─────────────────────────────────────────────────── @@ -83,7 +83,7 @@ public void Deposit_ThrowsOnNonPositiveAmount(decimal badAmount) public void Withdraw_AddsDebitTransaction() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Withdraw(40m); + a.Withdraw(new TransactionRequest { Amount = 40m }); Assert.Equal(2, a.TransactionCount); Assert.Equal(TransactionType.Debit, a.Transactions[1].Type); Assert.Equal(40m, a.Transactions[1].Amount); @@ -94,7 +94,7 @@ public void Withdraw_AddsDebitTransaction() public void Withdraw_ThrowsOnInsufficientFunds() { Account a = new Account("ACC-1000", "Ada", 100m); - Assert.Throws(() => a.Withdraw(500m)); + Assert.Throws(() => a.Withdraw(new TransactionRequest { Amount = 500m })); } [Fact] @@ -103,7 +103,7 @@ public void Withdraw_DoesNotRecordTransactionWhenItFails() Account a = new Account("ACC-1000", "Ada", 100m); try { - a.Withdraw(500m); + a.Withdraw(new TransactionRequest { Amount = 500m }); } catch (InsufficientFundsException) { @@ -119,7 +119,7 @@ public void Withdraw_DoesNotRecordTransactionWhenItFails() public void Withdraw_ThrowsOnNonPositiveAmount(decimal badAmount) { Account a = new Account("ACC-1000", "Ada", 100m); - Assert.Throws(() => a.Withdraw(badAmount)); + Assert.Throws(() => a.Withdraw(new TransactionRequest { Amount = badAmount })); } [Fact] @@ -146,7 +146,7 @@ public void SavingsAccount_ApplyInterest_AddsInterestCreditTransaction() public void CurrentAccount_Withdraw_AllowsBalanceToGoNegativeUpToOverdraftLimit() { CurrentAccount a = new CurrentAccount("ACC-1000", "Ada", 100m, 50m); - a.Withdraw(125m); + a.Withdraw(new TransactionRequest { Amount = 125m }); Assert.Equal(-25m, a.Balance); Assert.Equal(50m, a.OverdraftLimit); } @@ -155,7 +155,7 @@ public void CurrentAccount_Withdraw_AllowsBalanceToGoNegativeUpToOverdraftLimit( public void CurrentAccount_Withdraw_ThrowsWhenOverdraftLimitWouldBeExceeded() { CurrentAccount a = new CurrentAccount("ACC-1000", "Ada", 100m, 50m); - Assert.Throws(() => a.Withdraw(151m)); + Assert.Throws(() => a.Withdraw(new TransactionRequest { Amount = 151m })); } // ── Transactions (read-only view) ────────────────────────────── @@ -188,8 +188,8 @@ public void Statement_IncludesAccountNumberHolderAndBalance() public void Statement_ListsEveryTransaction() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); - a.Withdraw(30m); + a.Deposit(new TransactionRequest { Amount = 50m }); + a.Withdraw(new TransactionRequest { Amount = 30m }); string s = a.Statement(); Assert.Contains("Opening deposit", s); Assert.Contains("Deposit", s); @@ -212,9 +212,9 @@ public void Statement_ContainsMultipleLines() public void FindTransactions_ReturnsAllMatchesByDescription() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); - a.Withdraw(30m); - a.Deposit(20m); + a.Deposit(new TransactionRequest { Amount = 50m }); + a.Withdraw(new TransactionRequest { Amount = 30m }); + a.Deposit(new TransactionRequest { Amount = 20m }); // "Deposit" should match both "Opening deposit" (case-insensitive) // and the two "Deposit" entries. @@ -226,7 +226,7 @@ public void FindTransactions_ReturnsAllMatchesByDescription() public void FindTransactions_IsCaseInsensitive() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); + a.Deposit(new TransactionRequest { Amount = 50m }); Assert.Equal(2, a.FindTransactions("DEPOSIT").Count); Assert.Equal(2, a.FindTransactions("Deposit").Count); Assert.Equal(2, a.FindTransactions("deposit").Count); @@ -236,7 +236,7 @@ public void FindTransactions_IsCaseInsensitive() public void FindTransactions_ReturnsEmptyListWhenNothingMatches() { Account a = new Account("ACC-1000", "Ada", 100m); - a.Deposit(50m); + a.Deposit(new TransactionRequest { Amount = 50m }); List matches = a.FindTransactions("transfer"); Assert.NotNull(matches); Assert.Empty(matches); @@ -249,9 +249,9 @@ public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() // Small sleeps ensure distinct timestamps so the sort is verifiable. Thread.Sleep(15); - a.Deposit(50m); + a.Deposit(new TransactionRequest { Amount = 50m }); Thread.Sleep(15); - a.Deposit(20m); + a.Deposit(new TransactionRequest { Amount = 20m }); List matches = a.FindTransactions("deposit"); Assert.Equal(3, matches.Count); @@ -269,12 +269,14 @@ public void Statement_ReturnsTransactionsInRange() DateTime from = new DateTime(2026, 1, 10, 0, 0, 0, DateTimeKind.Utc); DateTime to = new DateTime(2026, 1, 15, 23, 59, 59, DateTimeKind.Utc); - a.Deposit(25m, new DateTime(2026, 1, 5, 9, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, "Too early"); - a.Deposit(50m, new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, - "In range deposit"); - a.Withdraw(20m, new DateTime(2026, 1, 15, 18, 30, 0, DateTimeKind.Utc), TransactionCategory.Other, - "In range withdrawal"); - a.Deposit(10m, new DateTime(2026, 1, 20, 9, 0, 0, DateTimeKind.Utc), TransactionCategory.Other, "Too late"); + a.Deposit(new TransactionRequest { Amount = 25m, Description = "Too early" }, + new DateTime(2026, 1, 5, 9, 0, 0, DateTimeKind.Utc)); + a.Deposit(new TransactionRequest { Amount = 50m, Description = "In range deposit" }, + new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc)); + a.Withdraw(new TransactionRequest { Amount = 20m, Description = "In range withdrawal" }, + new DateTime(2026, 1, 15, 18, 30, 0, DateTimeKind.Utc)); + a.Deposit(new TransactionRequest { Amount = 10m, Description = "Too late" }, + new DateTime(2026, 1, 20, 9, 0, 0, DateTimeKind.Utc)); string s = a.Statement(from, to); diff --git a/projects/bank/Bank.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index d53a96a..a6234d6 100644 --- a/projects/bank/Bank.Tests/BankTests.cs +++ b/projects/bank/Bank.Tests/BankTests.cs @@ -60,7 +60,7 @@ public void TotalAssets_SumsEveryAccountsBalance() b.OpenSavingsAccount("Ada", 100m); b.OpenSavingsAccount("Alan", 250m); Account grace = b.OpenCurrentAccount("Grace", 500m, 200m); - grace.Withdraw(50m); // Grace now 450 + grace.Withdraw(new TransactionRequest { Amount = 50m }); // Grace now 450 Assert.Equal(800m, b.TotalAssets); // 100 + 250 + 450 } @@ -189,7 +189,7 @@ public void ApplyInterest_UsesPolymorphismForMixedAccountTypes() Bank bank = new Bank("Acme"); Account savings = bank.OpenSavingsAccount("Ada", 100m); Account current = bank.OpenCurrentAccount("Alan", 0m, 100m); - current.Withdraw(50m); + current.Withdraw(new TransactionRequest { Amount = 50m }); bank.ApplyInterest(0.05m); Assert.Equal(105m, savings.Balance); Assert.Equal(-50m, current.Balance); diff --git a/projects/bank/Bank.Tests/TransactionTests.cs b/projects/bank/Bank.Tests/TransactionTests.cs index 15ee33a..8ce863d 100644 --- a/projects/bank/Bank.Tests/TransactionTests.cs +++ b/projects/bank/Bank.Tests/TransactionTests.cs @@ -5,7 +5,11 @@ public class TransactionTests [Fact] public void Constructor_AssignsAllProperties() { - Transaction t = new Transaction(TransactionType.Credit, 100m, TransactionCategory.Other, "Opening deposit"); + Transaction t = new Transaction(new TransactionProps + { + Type = TransactionType.Credit, Amount = 100m, Category = TransactionCategory.Other, + Description = "Opening deposit" + }); Assert.Equal(TransactionType.Credit, t.Type); Assert.Equal(100m, t.Amount); Assert.Equal("Opening deposit", t.Description); @@ -15,7 +19,8 @@ public void Constructor_AssignsAllProperties() public void Constructor_StampsTimestampCloseToNow() { DateTime before = DateTime.UtcNow; - Transaction t = new Transaction(TransactionType.Credit, 1m, TransactionCategory.Other, "x"); + Transaction t = new Transaction(new TransactionProps + { Type = TransactionType.Credit, Amount = 1m, Category = TransactionCategory.Other, Description = "x" }); DateTime after = DateTime.UtcNow; Assert.InRange(t.Timestamp, before, after); } @@ -27,7 +32,11 @@ public void Constructor_StampsTimestampCloseToNow() public void Constructor_ThrowsOnNonPositiveAmount(decimal badAmount) { Assert.Throws(() => - new Transaction(TransactionType.Credit, badAmount, TransactionCategory.Other, "x")); + new Transaction(new TransactionProps + { + Type = TransactionType.Credit, Amount = badAmount, Category = TransactionCategory.Other, + Description = "x" + })); } [Fact] diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index 1a297b5..dea45e6 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -72,8 +72,14 @@ public void Transfer(string fromAccountNumber, string toAccountNumber, decimal a throw new InvalidOperationException("To account not found"); } - fromAccount.Withdraw(amount, TransactionCategory.Transfer, $"Transfer to {toAccountNumber}"); - toAccount.Deposit(amount, TransactionCategory.Transfer, $"Transfer from {fromAccountNumber}"); + fromAccount.Withdraw(new TransactionRequest + { + Amount = amount, Category = TransactionCategory.Transfer, Description = $"Transfer to {toAccountNumber}" + }); + toAccount.Deposit(new TransactionRequest + { + Amount = amount, Category = TransactionCategory.Transfer, Description = $"Transfer from {fromAccountNumber}" + }); } public void ApplyInterest(decimal rate) diff --git a/projects/bank/Bank/Program.cs b/projects/bank/Bank/Program.cs index a07bb49..59c533a 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -2,10 +2,10 @@ Account a = bank.OpenSavingsAccount("Ada Lovelace", 200m); Account b = bank.OpenSavingsAccount("Alan Turing", 200m); Account c = bank.OpenCurrentAccount("Bob Smith", 0m, 100m); -c.Withdraw(50m); -a.Deposit(50m); -a.Withdraw(20m); -b.Deposit(20m); +c.Withdraw(new TransactionRequest { Amount = 50m }); +a.Deposit(new TransactionRequest { Amount = 50m }); +a.Withdraw(new TransactionRequest { Amount = 20m }); +b.Deposit(new TransactionRequest { Amount = 20m }); bank.Transfer(a.AccountNumber, b.AccountNumber, 50m); Console.WriteLine(a.Statement()); Console.WriteLine(b.Statement()); diff --git a/projects/bank/Bank/account/Account.cs b/projects/bank/Bank/account/Account.cs index 8795ba1..6c53372 100644 --- a/projects/bank/Bank/account/Account.cs +++ b/projects/bank/Bank/account/Account.cs @@ -20,8 +20,14 @@ public Account(string accountNumber, string holder, decimal startingBalance) _transactions = new Ledger(); if (startingBalance > 0) { - _transactions.Add(new Transaction(TransactionType.Credit, startingBalance, TransactionCategory.Other, - "Opening deposit")); + var transactionProps = new TransactionProps + { + Type = TransactionType.Credit, + Amount = startingBalance, + Category = TransactionCategory.Other, + Description = "Opening deposit" + }; + _transactions.Add(new Transaction(transactionProps)); } } @@ -53,58 +59,75 @@ public decimal Balance public IReadOnlyList Transactions => _transactions.Entries; - protected void RecordCredit(decimal amount, DateTime timestamp, TransactionCategory category, string description) + protected static TransactionProps CreateCreditProps(TransactionRequest req) { - decimal newBalance = Balance + amount; - string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Credit, amount, category, timestamp, desc)); - } + if (req.Amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } - protected void RecordDebit(decimal amount, DateTime timestamp, TransactionCategory category, string description) - { - decimal newBalance = Balance - amount; - string desc = description + $" (New Balance: {newBalance:N2})"; - _transactions.Add(new Transaction(TransactionType.Debit, amount, category, timestamp, desc)); + return new TransactionProps + { + Type = TransactionType.Credit, + Amount = req.Amount, + Category = req.Category ?? TransactionCategory.Other, + Description = req.Description ?? "Deposit", + }; } - public void Deposit(decimal amount, TransactionCategory category = TransactionCategory.Other, - string description = "Deposit") + protected void RecordCredit(TransactionProps props, DateTime timestamp) { - Deposit(amount, DateTime.UtcNow, category, description); + decimal newBalance = Balance + props.Amount; + string desc = props.Description + $" (New Balance: {newBalance:N2})"; + var transactionProps = props with + { + Description = desc, + }; + _transactions.Add(new Transaction(transactionProps, timestamp)); } - public void Deposit(decimal amount, DateTime timestamp, TransactionCategory category = TransactionCategory.Other, - string description = "Deposit") + protected static TransactionProps CreateDebitProps(TransactionRequest req) { - if (amount <= 0) + if (req.Amount <= 0) { throw new ArgumentException("Amount must be greater than 0"); } - RecordCredit(amount, timestamp, category, description); + return new TransactionProps + { + Type = TransactionType.Debit, + Amount = req.Amount, + Category = req.Category ?? TransactionCategory.Other, + Description = req.Description ?? "Withdrawal", + }; } - public void Withdraw(decimal amount, TransactionCategory category = TransactionCategory.Other, - string description = "Withdrawal") + protected void RecordDebit(TransactionProps props, DateTime timestamp) { - Withdraw(amount, DateTime.UtcNow, category, description); + decimal newBalance = Balance - props.Amount; + string desc = props.Description + $" (New Balance: {newBalance:N2})"; + var transactionProps = props with + { + Description = desc, + }; + _transactions.Add(new Transaction(transactionProps, timestamp)); } - public virtual void Withdraw(decimal amount, DateTime timestamp, - TransactionCategory category = TransactionCategory.Other, - string description = "Withdrawal") + public void Deposit(TransactionRequest req, DateTime? timestamp = null) { - if (amount <= 0) - { - throw new ArgumentException("Amount must be greater than 0"); - } + var transactionProps = CreateCreditProps(req); + RecordCredit(transactionProps, timestamp ?? DateTime.UtcNow); + } - if (amount > Balance) + public virtual void Withdraw(TransactionRequest req, DateTime? timestamp = null) + { + var transactionProps = CreateDebitProps(req); + if (transactionProps.Amount > Balance) { - throw new InsufficientFundsException(amount, Balance); + throw new InsufficientFundsException(transactionProps.Amount, Balance); } - RecordDebit(amount, timestamp, category, description); + RecordDebit(transactionProps, timestamp ?? DateTime.UtcNow); } public virtual void ApplyInterest(decimal rate) diff --git a/projects/bank/Bank/account/CurrentAccount.cs b/projects/bank/Bank/account/CurrentAccount.cs index 5024189..2f89c62 100644 --- a/projects/bank/Bank/account/CurrentAccount.cs +++ b/projects/bank/Bank/account/CurrentAccount.cs @@ -15,21 +15,15 @@ public CurrentAccount(string accountNumber, string holder, decimal startingBalan OverdraftLimit = overdraftLimit; } - public override void Withdraw(decimal amount, DateTime timestamp, - TransactionCategory category = TransactionCategory.Other, - string description = "Withdrawal") + public override void Withdraw(TransactionRequest req, DateTime? timestamp = null) { - if (amount <= 0) - { - throw new ArgumentException("Amount must be greater than 0"); - } - + var transactionProps = CreateDebitProps(req); decimal availableBalance = Balance + OverdraftLimit; - if (amount > availableBalance) + if (transactionProps.Amount > availableBalance) { - throw new InsufficientFundsException(amount, availableBalance); + throw new InsufficientFundsException(transactionProps.Amount, availableBalance); } - RecordDebit(amount, timestamp, category, description); + RecordDebit(transactionProps, timestamp ?? DateTime.UtcNow); } } diff --git a/projects/bank/Bank/account/SavingsAccount.cs b/projects/bank/Bank/account/SavingsAccount.cs index 06b320c..3a26a8f 100644 --- a/projects/bank/Bank/account/SavingsAccount.cs +++ b/projects/bank/Bank/account/SavingsAccount.cs @@ -19,6 +19,12 @@ public override void ApplyInterest(decimal rate) throw new InvalidOperationException("Balance must be greater than 0"); } - RecordCredit(Balance * rate, DateTime.UtcNow, TransactionCategory.Interest, $"Interest {rate:P2}"); + var transactionProps = CreateCreditProps(new TransactionRequest + { + Amount = Balance * rate, + Category = TransactionCategory.Interest, + Description = $"Interest {rate:P2}" + }); + RecordCredit(transactionProps, DateTime.UtcNow); } } diff --git a/projects/bank/Bank/transaction/Transaction.cs b/projects/bank/Bank/transaction/Transaction.cs index 1c3dc9c..3d072f8 100644 --- a/projects/bank/Bank/transaction/Transaction.cs +++ b/projects/bank/Bank/transaction/Transaction.cs @@ -8,27 +8,21 @@ public struct Transaction public DateTime Timestamp { get; } public string Description { get; } - public Transaction(TransactionType type, decimal amount, TransactionCategory category, string description) - : this(type, amount, category, DateTime.UtcNow, description) + public Transaction(TransactionProps props) : this(props, DateTime.UtcNow) { } - public Transaction( - TransactionType type, - decimal amount, - TransactionCategory category, - DateTime timestamp, - string description) + public Transaction(TransactionProps props, DateTime timestamp) { - if (amount <= 0) + if (props.Amount <= 0) { throw new ArgumentException("Amount must be greater than 0"); } - Type = type; - Amount = amount; - Category = category; + Type = props.Type; + Amount = props.Amount; + Category = props.Category; Timestamp = timestamp; - Description = description; + Description = props.Description; } } \ No newline at end of file diff --git a/projects/bank/Bank/transaction/TransactionProps.cs b/projects/bank/Bank/transaction/TransactionProps.cs new file mode 100644 index 0000000..f135c0d --- /dev/null +++ b/projects/bank/Bank/transaction/TransactionProps.cs @@ -0,0 +1,9 @@ +namespace BankApp.transaction; + +public record TransactionProps +{ + public required TransactionType Type { get; init; } + public required decimal Amount { get; init; } + public required TransactionCategory Category { get; init; } + public required string Description { get; init; } +} \ No newline at end of file diff --git a/projects/bank/Bank/transaction/TransactionRequest.cs b/projects/bank/Bank/transaction/TransactionRequest.cs new file mode 100644 index 0000000..56c6312 --- /dev/null +++ b/projects/bank/Bank/transaction/TransactionRequest.cs @@ -0,0 +1,8 @@ +namespace BankApp.transaction; + +public record TransactionRequest +{ + public decimal Amount { get; init; } + public TransactionCategory? Category { get; init; } + public string? Description { get; init; } +} \ No newline at end of file From 788fd5dc5e51bb2b8d38fcd725aaaf4ed65353b7 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:00:25 +0200 Subject: [PATCH 26/27] timestamp --- projects/bank/Bank/account/Account.cs | 12 ++++++++---- projects/bank/Bank/account/CurrentAccount.cs | 2 +- projects/bank/Bank/account/SavingsAccount.cs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/projects/bank/Bank/account/Account.cs b/projects/bank/Bank/account/Account.cs index 6c53372..f154f13 100644 --- a/projects/bank/Bank/account/Account.cs +++ b/projects/bank/Bank/account/Account.cs @@ -75,7 +75,7 @@ protected static TransactionProps CreateCreditProps(TransactionRequest req) }; } - protected void RecordCredit(TransactionProps props, DateTime timestamp) + protected void RecordCredit(TransactionProps props, DateTime? timestamp = null) { decimal newBalance = Balance + props.Amount; string desc = props.Description + $" (New Balance: {newBalance:N2})"; @@ -83,7 +83,9 @@ protected void RecordCredit(TransactionProps props, DateTime timestamp) { Description = desc, }; - _transactions.Add(new Transaction(transactionProps, timestamp)); + _transactions.Add(timestamp is not null + ? new Transaction(transactionProps, timestamp.Value) + : new Transaction(transactionProps)); } protected static TransactionProps CreateDebitProps(TransactionRequest req) @@ -102,7 +104,7 @@ protected static TransactionProps CreateDebitProps(TransactionRequest req) }; } - protected void RecordDebit(TransactionProps props, DateTime timestamp) + protected void RecordDebit(TransactionProps props, DateTime? timestamp = null) { decimal newBalance = Balance - props.Amount; string desc = props.Description + $" (New Balance: {newBalance:N2})"; @@ -110,7 +112,9 @@ protected void RecordDebit(TransactionProps props, DateTime timestamp) { Description = desc, }; - _transactions.Add(new Transaction(transactionProps, timestamp)); + _transactions.Add(timestamp is not null + ? new Transaction(transactionProps, timestamp.Value) + : new Transaction(transactionProps)); } public void Deposit(TransactionRequest req, DateTime? timestamp = null) diff --git a/projects/bank/Bank/account/CurrentAccount.cs b/projects/bank/Bank/account/CurrentAccount.cs index 2f89c62..799ec49 100644 --- a/projects/bank/Bank/account/CurrentAccount.cs +++ b/projects/bank/Bank/account/CurrentAccount.cs @@ -24,6 +24,6 @@ public override void Withdraw(TransactionRequest req, DateTime? timestamp = null throw new InsufficientFundsException(transactionProps.Amount, availableBalance); } - RecordDebit(transactionProps, timestamp ?? DateTime.UtcNow); + RecordDebit(transactionProps, timestamp); } } diff --git a/projects/bank/Bank/account/SavingsAccount.cs b/projects/bank/Bank/account/SavingsAccount.cs index 3a26a8f..3951517 100644 --- a/projects/bank/Bank/account/SavingsAccount.cs +++ b/projects/bank/Bank/account/SavingsAccount.cs @@ -25,6 +25,6 @@ public override void ApplyInterest(decimal rate) Category = TransactionCategory.Interest, Description = $"Interest {rate:P2}" }); - RecordCredit(transactionProps, DateTime.UtcNow); + RecordCredit(transactionProps); } } From 8b82d8aee03a789bccb1caa9a671cc81c65327d3 Mon Sep 17 00:00:00 2001 From: Albin Carlsson <89704855+EpicAlbin03@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:30:48 +0200 Subject: [PATCH 27/27] single utcnow --- projects/bank/Bank/account/Account.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/bank/Bank/account/Account.cs b/projects/bank/Bank/account/Account.cs index f154f13..9c4cb38 100644 --- a/projects/bank/Bank/account/Account.cs +++ b/projects/bank/Bank/account/Account.cs @@ -120,7 +120,7 @@ protected void RecordDebit(TransactionProps props, DateTime? timestamp = null) public void Deposit(TransactionRequest req, DateTime? timestamp = null) { var transactionProps = CreateCreditProps(req); - RecordCredit(transactionProps, timestamp ?? DateTime.UtcNow); + RecordCredit(transactionProps, timestamp); } public virtual void Withdraw(TransactionRequest req, DateTime? timestamp = null) @@ -131,7 +131,7 @@ public virtual void Withdraw(TransactionRequest req, DateTime? timestamp = null) throw new InsufficientFundsException(transactionProps.Amount, Balance); } - RecordDebit(transactionProps, timestamp ?? DateTime.UtcNow); + RecordDebit(transactionProps, timestamp); } public virtual void ApplyInterest(decimal rate)