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 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/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..1a109bc --- /dev/null +++ b/csharp.md @@ -0,0 +1,90 @@ +**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() +- 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: + +- :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() +- 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** + +- .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: 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 +- 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** + +- increasing int under the hood +- ToString() returns name not int + +**Exceptions** + +- try/catch +- use specific built-in csharp error (Exception for all, avoid) 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/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/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/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/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/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/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; } } 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(); } } 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); } } diff --git a/projects/bank/Bank.Tests/AccountTests.cs b/projects/bank/Bank.Tests/AccountTests.cs index c959a97..13d5a35 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 @@ -35,8 +33,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 ──────────────────────────────────────────────────── @@ -52,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); } @@ -64,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); @@ -77,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 ─────────────────────────────────────────────────── @@ -86,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); @@ -97,14 +94,21 @@ 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] public void Withdraw_DoesNotRecordTransactionWhenItFails() { Account a = new Account("ACC-1000", "Ada", 100m); - try { a.Withdraw(500m); } catch (InvalidOperationException) { } + try + { + a.Withdraw(new TransactionRequest { Amount = 500m }); + } + catch (InsufficientFundsException) + { + } + Assert.Equal(1, a.TransactionCount); // only the opening deposit Assert.Equal(100m, a.Balance); } @@ -115,7 +119,43 @@ 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] + 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(new TransactionRequest { Amount = 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(new TransactionRequest { Amount = 151m })); } // ── Transactions (read-only view) ────────────────────────────── @@ -124,6 +164,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); @@ -147,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); @@ -171,9 +212,10 @@ 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. List matches = a.FindTransactions("deposit"); @@ -184,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); @@ -194,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); @@ -204,11 +246,12 @@ 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); - a.Deposit(50m); - System.Threading.Thread.Sleep(15); - a.Deposit(20m); + Thread.Sleep(15); + a.Deposit(new TransactionRequest { Amount = 50m }); + Thread.Sleep(15); + a.Deposit(new TransactionRequest { Amount = 20m }); List matches = a.FindTransactions("deposit"); Assert.Equal(3, matches.Count); @@ -217,4 +260,29 @@ public void FindTransactions_ReturnsResultsSortedByTimestampOldestFirst() Assert.True(matches[i - 1].Timestamp <= matches[i].Timestamp); } } + + + [Fact] + public void Statement_ReturnsTransactionsInRange() + { + 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(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); + + 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.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.Tests/BankTests.cs b/projects/bank/Bank.Tests/BankTests.cs index fa083d6..a6234d6 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 @@ -14,33 +12,44 @@ public void Constructor_AssignsNameAndStartsEmpty() } [Fact] - public void OpenAccount_ReturnsAccountWithAutoAssignedNumber() + public void OpenSavingsAccount_ReturnsSavingsAccountWithAutoAssignedNumber() + { + Bank b = new Bank("Acme"); + 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 OpenCurrentAccount_ReturnsCurrentAccountWithOverdraftLimit() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); + 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 OpenAccount_IncrementsAccountNumbers() + public void OpenAccountMethods_IncrementAccountNumbers() { Bank b = new Bank("Acme"); - Account a = b.OpenAccount("Ada", 100m); - Account c = b.OpenAccount("Alan", 200m); - Account d = b.OpenAccount("Grace", 300m); + 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); } @@ -48,10 +57,10 @@ 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); - grace.Withdraw(50m); // Grace now 450 + b.OpenSavingsAccount("Ada", 100m); + b.OpenSavingsAccount("Alan", 250m); + Account grace = b.OpenCurrentAccount("Grace", 500m, 200m); + grace.Withdraw(new TransactionRequest { Amount = 50m }); // Grace now 450 Assert.Equal(800m, b.TotalAssets); // 100 + 250 + 450 } @@ -59,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); @@ -70,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")); } @@ -78,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); @@ -90,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); } @@ -99,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); } @@ -110,9 +119,79 @@ 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); } + + [Fact] + public void Transfer_TransfersFundsBetweenAccounts() + { + Bank bank = new Bank("Acme"); + 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.OpenSavingsAccount("Alan", 200m); + Assert.Throws(() => bank.Transfer("ACC-9999", b.AccountNumber, 50m)); + } + + [Fact] + public void Transfer_ThrowsWhenToAccountNotFound() + { + Bank bank = new Bank("Acme"); + Account a = bank.OpenSavingsAccount("Ada", 100m); + Assert.Throws(() => bank.Transfer(a.AccountNumber, "ACC-9999", 50m)); + } + + [Fact] + public void Transfer_ThrowsWhenInsufficientFunds() + { + Bank bank = new Bank("Acme"); + 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_CreditsInterestToSavingsAccountsWithPositiveBalance() + { + Bank bank = new Bank("Acme"); + 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_UsesPolymorphismForMixedAccountTypes() + { + Bank bank = new Bank("Acme"); + Account savings = bank.OpenSavingsAccount("Ada", 100m); + Account current = bank.OpenCurrentAccount("Alan", 0m, 100m); + 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 dc39ecd..8ce863d 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 @@ -7,7 +5,11 @@ public class TransactionTests [Fact] public void Constructor_AssignsAllProperties() { - Transaction t = new Transaction(TransactionType.Credit, 100m, "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); @@ -17,7 +19,8 @@ public void Constructor_AssignsAllProperties() public void Constructor_StampsTimestampCloseToNow() { DateTime before = DateTime.UtcNow; - Transaction t = new Transaction(TransactionType.Credit, 1m, "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); } @@ -28,8 +31,12 @@ 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(new TransactionProps + { + Type = TransactionType.Credit, Amount = badAmount, Category = TransactionCategory.Other, + Description = "x" + })); } [Fact] diff --git a/projects/bank/Bank/Account.cs b/projects/bank/Bank/Account.cs deleted file mode 100644 index 2993428..0000000 --- a/projects/bank/Bank/Account.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Text; - -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; - - public string AccountNumber { get; } - public string Holder { get; } - - 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\"."); - } - - // 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"); } - } - - public int TransactionCount - { - get { throw new NotImplementedException("TODO: 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()"); } - } - - public void Deposit(decimal amount) - { - throw new NotImplementedException("TODO: throw ArgumentException if amount <= 0, then add a Credit transaction with description \"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."); - } - - // 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() - { - 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."); - } - - // 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"); - } -} diff --git a/projects/bank/Bank/Bank.cs b/projects/bank/Bank/Bank.cs index ca1ab8a..dea45e6 100644 --- a/projects/bank/Bank/Bank.cs +++ b/projects/bank/Bank/Bank.cs @@ -1,53 +1,92 @@ 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) { - 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"); } - } + public int AccountCount => _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 + public IReadOnlyList Accounts => _accounts.AsReadOnly(); + + public SavingsAccount OpenSavingsAccount(string holder, decimal startingBalance) { - get { throw new NotImplementedException("TODO: return accounts.AsReadOnly()"); } + string accountNumber = $"ACC-{_nextAccountNumber}"; + _nextAccountNumber++; + SavingsAccount account = new SavingsAccount(accountNumber, holder, startingBalance); + _accounts.Add(account); + return account; } - public Account OpenAccount(string holder, decimal startingBalance) + public CurrentAccount OpenCurrentAccount(string holder, decimal startingBalance, decimal overdraftLimit) { - 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++; + CurrentAccount account = new CurrentAccount(accountNumber, holder, startingBalance, overdraftLimit); + _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; + } + + 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"); + } + + 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) + { + foreach (Account account in _accounts) + { + account.ApplyInterest(rate); + } } } 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 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 e5b6ffe..59c533a 100644 --- a/projects/bank/Bank/Program.cs +++ b/projects/bank/Bank/Program.cs @@ -1,62 +1,19 @@ -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}"); -} - -// 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…). +Account a = bank.OpenSavingsAccount("Ada Lovelace", 200m); +Account b = bank.OpenSavingsAccount("Alan Turing", 200m); +Account c = bank.OpenCurrentAccount("Bob Smith", 0m, 100m); +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()); +Console.WriteLine(c.Statement()); +Console.WriteLine(bank.TotalAssets); + +bank.ApplyInterest(0.05m); + +Console.WriteLine(a.Statement()); +Console.WriteLine(b.Statement()); +Console.WriteLine(c.Statement()); \ No newline at end of file diff --git a/projects/bank/Bank/Transaction.cs b/projects/bank/Bank/Transaction.cs deleted file mode 100644 index 7a75e46..0000000 --- a/projects/bank/Bank/Transaction.cs +++ /dev/null @@ -1,29 +0,0 @@ -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; } - public decimal Amount { get; } - public DateTime Timestamp { get; } - public string Description { get; } - - 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"); - } -} diff --git a/projects/bank/Bank/TransactionType.cs b/projects/bank/Bank/TransactionType.cs deleted file mode 100644 index a464631..0000000 --- a/projects/bank/Bank/TransactionType.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 -} diff --git a/projects/bank/Bank/account/Account.cs b/projects/bank/Bank/account/Account.cs new file mode 100644 index 0000000..9c4cb38 --- /dev/null +++ b/projects/bank/Bank/account/Account.cs @@ -0,0 +1,180 @@ +namespace BankApp.account; + +public class Account +{ + private readonly Ledger _transactions; + + public string AccountNumber { get; } + public string Holder { get; } + public virtual decimal OverdraftLimit => 0m; + + public Account(string accountNumber, string holder, decimal startingBalance) + { + if (startingBalance < 0) + { + throw new ArgumentException("Starting balance must be greater than 0"); + } + + AccountNumber = accountNumber; + Holder = holder; + _transactions = new Ledger(); + if (startingBalance > 0) + { + var transactionProps = new TransactionProps + { + Type = TransactionType.Credit, + Amount = startingBalance, + Category = TransactionCategory.Other, + Description = "Opening deposit" + }; + _transactions.Add(new Transaction(transactionProps)); + } + } + + public decimal Balance + { + get + { + decimal total = 0; + foreach (Transaction transaction in _transactions) + { + switch (transaction.Type) + { + case TransactionType.Credit: + total += transaction.Amount; + break; + case TransactionType.Debit: + total -= transaction.Amount; + break; + default: + throw new ArgumentOutOfRangeException(nameof(transaction.Type)); + } + } + + return total; + } + } + + public int TransactionCount => _transactions.Count; + + public IReadOnlyList Transactions => _transactions.Entries; + + protected static TransactionProps CreateCreditProps(TransactionRequest req) + { + if (req.Amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + return new TransactionProps + { + Type = TransactionType.Credit, + Amount = req.Amount, + Category = req.Category ?? TransactionCategory.Other, + Description = req.Description ?? "Deposit", + }; + } + + protected void RecordCredit(TransactionProps props, DateTime? timestamp = null) + { + decimal newBalance = Balance + props.Amount; + string desc = props.Description + $" (New Balance: {newBalance:N2})"; + var transactionProps = props with + { + Description = desc, + }; + _transactions.Add(timestamp is not null + ? new Transaction(transactionProps, timestamp.Value) + : new Transaction(transactionProps)); + } + + protected static TransactionProps CreateDebitProps(TransactionRequest req) + { + if (req.Amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + return new TransactionProps + { + Type = TransactionType.Debit, + Amount = req.Amount, + Category = req.Category ?? TransactionCategory.Other, + Description = req.Description ?? "Withdrawal", + }; + } + + protected void RecordDebit(TransactionProps props, DateTime? timestamp = null) + { + decimal newBalance = Balance - props.Amount; + string desc = props.Description + $" (New Balance: {newBalance:N2})"; + var transactionProps = props with + { + Description = desc, + }; + _transactions.Add(timestamp is not null + ? new Transaction(transactionProps, timestamp.Value) + : new Transaction(transactionProps)); + } + + public void Deposit(TransactionRequest req, DateTime? timestamp = null) + { + var transactionProps = CreateCreditProps(req); + RecordCredit(transactionProps, timestamp); + } + + public virtual void Withdraw(TransactionRequest req, DateTime? timestamp = null) + { + var transactionProps = CreateDebitProps(req); + if (transactionProps.Amount > Balance) + { + throw new InsufficientFundsException(transactionProps.Amount, Balance); + } + + RecordDebit(transactionProps, timestamp); + } + + public virtual void ApplyInterest(decimal rate) + { + } + + private string BuildStatement(IEnumerable includedTransactions) + { + StringBuilder sb = new StringBuilder(); + 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 includedTransactions) + { + sb.AppendLine( + $"{transaction.Timestamp:yyyy-MM-dd HH:mm} {transaction.Type.ToString().ToUpper()} {transaction.Amount:N2} {transaction.Description}"); + } + + return sb.ToString(); + } + + 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); + } + + 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/account/CurrentAccount.cs b/projects/bank/Bank/account/CurrentAccount.cs new file mode 100644 index 0000000..799ec49 --- /dev/null +++ b/projects/bank/Bank/account/CurrentAccount.cs @@ -0,0 +1,29 @@ +namespace BankApp.account; + +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(TransactionRequest req, DateTime? timestamp = null) + { + var transactionProps = CreateDebitProps(req); + decimal availableBalance = Balance + OverdraftLimit; + if (transactionProps.Amount > availableBalance) + { + throw new InsufficientFundsException(transactionProps.Amount, availableBalance); + } + + RecordDebit(transactionProps, timestamp); + } +} diff --git a/projects/bank/Bank/account/Ledger.cs b/projects/bank/Bank/account/Ledger.cs new file mode 100644 index 0000000..c61a656 --- /dev/null +++ b/projects/bank/Bank/account/Ledger.cs @@ -0,0 +1,27 @@ +using System.Collections; + +namespace BankApp.account; + +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 diff --git a/projects/bank/Bank/account/SavingsAccount.cs b/projects/bank/Bank/account/SavingsAccount.cs new file mode 100644 index 0000000..3951517 --- /dev/null +++ b/projects/bank/Bank/account/SavingsAccount.cs @@ -0,0 +1,30 @@ +namespace BankApp.account; + +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"); + } + + var transactionProps = CreateCreditProps(new TransactionRequest + { + Amount = Balance * rate, + Category = TransactionCategory.Interest, + Description = $"Interest {rate:P2}" + }); + RecordCredit(transactionProps); + } +} diff --git a/projects/bank/Bank/transaction/Transaction.cs b/projects/bank/Bank/transaction/Transaction.cs new file mode 100644 index 0000000..3d072f8 --- /dev/null +++ b/projects/bank/Bank/transaction/Transaction.cs @@ -0,0 +1,28 @@ +namespace BankApp.transaction; + +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(TransactionProps props) : this(props, DateTime.UtcNow) + { + } + + public Transaction(TransactionProps props, DateTime timestamp) + { + if (props.Amount <= 0) + { + throw new ArgumentException("Amount must be greater than 0"); + } + + Type = props.Type; + Amount = props.Amount; + Category = props.Category; + Timestamp = timestamp; + Description = props.Description; + } +} \ No newline at end of file diff --git a/projects/bank/Bank/transaction/TransactionCategory.cs b/projects/bank/Bank/transaction/TransactionCategory.cs new file mode 100644 index 0000000..e711650 --- /dev/null +++ b/projects/bank/Bank/transaction/TransactionCategory.cs @@ -0,0 +1,12 @@ +namespace BankApp.transaction; + +public enum TransactionCategory +{ + Food, + Rent, + Salary, + Transfer, + Interest, + Fees, + Other +} 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 diff --git a/projects/bank/Bank/transaction/TransactionType.cs b/projects/bank/Bank/transaction/TransactionType.cs new file mode 100644 index 0000000..5ad1d4e --- /dev/null +++ b/projects/bank/Bank/transaction/TransactionType.cs @@ -0,0 +1,7 @@ +namespace BankApp.transaction; + +public enum TransactionType +{ + Credit, // Credit → money INTO the account (starting deposit, deposits) + Debit // Debit → money OUT of the account (withdrawals) +} diff --git a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs index d0d5afe..042f792 100644 --- a/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/DirectoryNodeTests.cs @@ -123,4 +123,167 @@ 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()); + } + + [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")); + } + + [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()); + } + + [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()); + } + + [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 07de079..6f7cbaf 100644 --- a/projects/filesystem/FileSystem.Tests/FileNodeTests.cs +++ b/projects/filesystem/FileSystem.Tests/FileNodeTests.cs @@ -73,4 +73,56 @@ 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()); + } + + [Fact] + 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()); + } + + [Fact] + 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 e59ed7d..b93f916 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,78 @@ 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; + } + + public override List FilterByExtension(string ext) + { + List result = []; + foreach (FSNode child in children) + { + result.AddRange(child.FilterByExtension(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; + } + + 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 8bd6817..9705c0a 100644 --- a/projects/filesystem/FileSystem/FSNode.cs +++ b/projects/filesystem/FileSystem/FSNode.cs @@ -40,4 +40,14 @@ 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(); + + public abstract List FilterByExtension(string ext); + + 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 b83d19d..a3a710a 100644 --- a/projects/filesystem/FileSystem/FileNode.cs +++ b/projects/filesystem/FileSystem/FileNode.cs @@ -44,4 +44,39 @@ public override void Print(int indent = 0) { return Name == name ? this : null; } + + public override FileNode LargestFile() + { + return this; + } + + public override List FilterByExtension(string ext) + { + return Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase) + ? [this] + : new List(); + } + + public override Dictionary CountByExtension() + { + return new Dictionary { { Path.GetExtension(Name).ToLowerInvariant(), 1 } }; + } + + 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(); + 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}]"; } }