Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 154 additions & 1 deletion 03-LINQ/GoldSavings.App/DataServices/GoldAnalysisService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.IO;
using GoldSavings.App.Model;

namespace GoldSavings.App.Services
Expand All @@ -12,10 +14,161 @@ public class GoldAnalysisService
public GoldAnalysisService(List<GoldPrice> goldPrices)
{
_goldPrices = goldPrices;
Console.WriteLine($"GoldAnalysisService initialized with {_goldPrices.Count} records.");
}

public double GetAveragePrice()
{
if (!_goldPrices.Any()) {
Console.WriteLine("Warning: No gold prices available to calculate average.");
return 0;
}
return _goldPrices.Average(p => p.Price);
}
}

public (List<GoldPrice> topThreeHighest, List<GoldPrice> topThreeLowest) GetThreeHigestAndLowestGoldPrices()
{
var prices2024 = _goldPrices.Where(p => p.Date.Year == 2024).ToList();

var topThreeHighest = prices2024
.OrderByDescending(p => p.Price)
.Take(3)
.ToList();

var topThreeLowest = prices2024
.OrderBy(p => p.Price)
.Take(3)
.ToList();

return (topThreeHighest, topThreeLowest);
}

public List<DateTime> GetProfitabilitySinceJanuary2020()
{
var january2020Prices = _goldPrices
.Where(g => g.Date.Year == 2020 && g.Date.Month == 1)
.Select(g => g.Price)
.ToList();

if (!january2020Prices.Any())
{
Console.WriteLine("Warning: No data for January 2020. Skipping profitability analysis.");
return new List<DateTime>();
}

var minPriceJan2020 = january2020Prices.Min();
var threshold = minPriceJan2020 * 1.05;

return _goldPrices
.Where(g => g.Price >= threshold && g.Date.Year >= 2020)
.Select(g => g.Date)
.ToList();
}

public List<DateTime> GetSecondTenPriceRankingDates()
{
var filteredPrices = _goldPrices
.Where(g => g.Date.Year >= 2019 && g.Date.Year <= 2022)
.OrderByDescending(g => g.Price)
.ToList();

Console.WriteLine($"Gold prices found in range 2019-2022: {filteredPrices.Count}");

if (filteredPrices.Count < 13)
{
Console.WriteLine("Not enough records to extract the second ten of the ranking.");
return new List<DateTime>();
}

return filteredPrices.Skip(10).Take(3).Select(g => g.Date).ToList();
}

public Dictionary<int, double> GetAverageGoldPrices(params int[] years)
{
var filteredPrices = _goldPrices.Where(g => years.Contains(g.Date.Year)).ToList();

if (!filteredPrices.Any())
{
Console.WriteLine("Warning: No gold price data available for the requested years.");
return new Dictionary<int, double>();
}

return filteredPrices
.GroupBy(g => g.Date.Year)
.ToDictionary(g => g.Key, g => g.Average(p => p.Price));
}

public (DateTime BuyDate, double BuyPrice, DateTime SellDate, double SellPrice, double Profit, double ROI) GetBestTimeToBuyAndSellGold(int? startYear = null, int? endYear = null)
{
var pricesToAnalyze = _goldPrices;
if (startYear.HasValue || endYear.HasValue)
{
pricesToAnalyze = _goldPrices
.Where(p => (!startYear.HasValue || p.Date.Year >= startYear.Value) &&
(!endYear.HasValue || p.Date.Year <= endYear.Value))
.ToList();
}

if (!pricesToAnalyze.Any())
{
Console.WriteLine("Error: No gold prices available for investment analysis.");
return (DateTime.MinValue, 0, DateTime.MinValue, 0, 0, 0);
}

var bestBuy = pricesToAnalyze.OrderBy(p => p.Price).FirstOrDefault();
var bestSell = pricesToAnalyze.Where(p => p.Date > bestBuy.Date)
.OrderByDescending(p => p.Price)
.FirstOrDefault();

if (bestBuy == null || bestSell == null || bestBuy.Date >= bestSell.Date)
{
Console.WriteLine("Error: Could not determine valid buy/sell dates.");
return (DateTime.MinValue, 0, DateTime.MinValue, 0, 0, 0);
}

double profit = bestSell.Price - bestBuy.Price;
double returnOnInvestment = (profit / bestBuy.Price) * 100;

return (bestBuy.Date, bestBuy.Price, bestSell.Date, bestSell.Price, profit, returnOnInvestment);
}

// list of prices to a file in XML format
public void SavePricesToXml(string filePath)
{
try
{
var xml = new XDocument(
new XElement("GoldPrices",
_goldPrices.Select(p =>
new XElement("GoldPrice",
new XElement("Date", p.Date.ToString("yyyy-MM-dd")),
new XElement("Price", p.Price)
)
)
)
);

xml.Save(filePath);
Console.WriteLine($"Gold prices successfully saved to {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving gold prices to XML: {ex.Message}");
}
}

// read the contents of the XML file from the previous set using one instruction
public List<GoldPrice> LoadPricesFromXml(string filePath) =>
File.Exists(filePath) ?
XDocument.Load(filePath)
.Descendants("GoldPrice")
.Select(p => new GoldPrice {
Date = DateTime.Parse(p.Element("Date")?.Value ?? "0001-01-01"),
Price = double.TryParse(p.Element("Price")?.Value, out double price) ? price : 0
})
.ToList()
: new List<GoldPrice>();


}
}
47 changes: 47 additions & 0 deletions 03-LINQ/GoldSavings.App/DataServices/RandomList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;

namespace GoldSavings.App.Services
{
// generic collection provides randomized operations on a list.
public class RandomList<T>
{
private readonly List<T> _items = new List<T>();
private readonly Random _random = new Random();

// adds element to the list at a random position (either beginning or end)
public void Add(T item)
{
// 50% chance to add at beginning or end
if (_random.Next(2) == 0)
{
_items.Insert(0, item); //beginning
}
else
{
_items.Add(item); //end
}
}

// Gets an element from random position up to the specified index
public T Get(int index)
{
if (IsEmpty)
{
throw new InvalidOperationException("Cannot get items from empty collection");
}

// so we don't exceed the list bounds:
int maxIndex = Math.Min(index, _items.Count - 1);
int randomIndex = _random.Next(maxIndex + 1);

return _items[randomIndex];
}

public bool IsEmpty => _items.Count == 0; // sees whether the collection contains any elements


public int Count => _items.Count; // gets the total number of elements in the collection

}
}
24 changes: 24 additions & 0 deletions 03-LINQ/GoldSavings.App/GoldSavings.App.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoldSavings.App", "GoldSavings.App.csproj", "{D6E0D941-961A-D0DA-4517-688BD74A41F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D6E0D941-961A-D0DA-4517-688BD74A41F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6E0D941-961A-D0DA-4517-688BD74A41F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6E0D941-961A-D0DA-4517-688BD74A41F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6E0D941-961A-D0DA-4517-688BD74A41F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F417F327-7351-4764-A418-1158B360A2E4}
EndGlobalSection
EndGlobal
96 changes: 88 additions & 8 deletions 03-LINQ/GoldSavings.App/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GoldSavings.App.Model;
using GoldSavings.App.Model;
using GoldSavings.App.Client;
using GoldSavings.App.Services;
namespace GoldSavings.App;
Expand All @@ -9,28 +9,108 @@ static void Main(string[] args)
{
Console.WriteLine("Hello, Gold Investor!");

// Step 1: Get gold prices
/*/ Step 1: Get gold prices
GoldDataService dataService = new GoldDataService();
DateTime startDate = new DateTime(2024,09,18);
DateTime endDate = DateTime.Now;
DateTime startDate = new DateTime(2024,01,01);
DateTime endDate = new DateTime(2024,12,31);
List<GoldPrice> goldPrices = dataService.GetGoldPrices(startDate, endDate).GetAwaiter().GetResult();

if (goldPrices.Count == 0)
// additional historical data (2019-2023)
List<GoldPrice> historicalPrices = dataService.GetGoldPrices(new DateTime(2019, 01, 01), new DateTime(2023, 12, 31))
.GetAwaiter().GetResult();
*/
GoldDataService dataService = new GoldDataService();
List<GoldPrice> allPrices = new List<GoldPrice>();

for (int year = 2019; year <= 2024; year++)
{
List<GoldPrice> yearlyPrices = dataService.GetGoldPrices(
new DateTime(year, 01, 01),
new DateTime(year, 12, 31)
).GetAwaiter().GetResult();

Console.WriteLine($"Retrieved {yearlyPrices.Count} records for {year}.");
allPrices.AddRange(yearlyPrices);
}

if (!allPrices.Any()) {
Console.WriteLine("No data found. Exiting.");
return;
}

Console.WriteLine($"Retrieved {goldPrices.Count} records. Ready for analysis.");
Console.WriteLine($"Total records retrieved: {allPrices.Count}. Ready for analysis.\n");

/*/ Combine all data for analysis
List<GoldPrice> allPrices = goldPrices.Concat(historicalPrices).ToList();

if (goldPrices.Count == 0){
Console.WriteLine("No data found. Exiting.");
return;
}
Console.WriteLine($"Retrieved {goldPrices.Count} records for 2024.");
Console.WriteLine($"Retrieved {historicalPrices.Count} historical records (2019-2023). Ready for analysis.\n");

// variables for analysis results
Dictionary<int, double> avgPrices;
List<DateTime> profitableDays;
List<DateTime> secondTenDates;
(DateTime BuyDate, double BuyPrice, DateTime SellDate, double SellPrice, double Profit, double ROI) bestTrade = (DateTime.MinValue, 0, DateTime.MinValue, 0, 0, 0); // Default initialization
*/

// Step 2: Perform analysis
GoldAnalysisService analysisService = new GoldAnalysisService(goldPrices);
GoldAnalysisService analysisService = new GoldAnalysisService(allPrices);
var avgPrice = analysisService.GetAveragePrice();
var (topThreeHighest, topThreeLowest) = analysisService.GetThreeHigestAndLowestGoldPrices();

var profitableDays = analysisService.GetProfitabilitySinceJanuary2020();
var secondTenDates = analysisService.GetSecondTenPriceRankingDates();
var bestTrade = analysisService.GetBestTimeToBuyAndSellGold(2020, 2024);
var avgPrices = analysisService.GetAverageGoldPrices(allPrices.Select(g => g.Date.Year).Distinct().ToArray());


// Step 3: Print results
GoldResultPrinter.PrintSingleValue(Math.Round(avgPrice, 2), "Average Gold Price Last Half Year");
Console.WriteLine("\nGold Analyis Queries with LINQ Completed.\n");

Console.WriteLine($"Top 3 highest gold prices (in 2024): {string.Join(", ", topThreeHighest.Select(p => p.Price))}");
Console.WriteLine($"Top 3 lowest gold prices (in 2024): {string.Join(", ", topThreeLowest.Select(p => p.Price))}\n");

bool earnedMoreThan5Percent = profitableDays.Any();
Console.WriteLine($"If one had bought gold in January 2020, is it possible that they would have earned more than 5%?: {(earnedMoreThan5Percent ? "Yes" : "No")}");
if (earnedMoreThan5Percent) {
Console.WriteLine($"If so, on these days: {string.Join(", ", profitableDays.Select(d => d.ToString("yyyy-MM-dd")))}");
}
Console.WriteLine();

Console.WriteLine("The 3 dates of 2019-2022 that open the second ten of the prices ranking are:");
Console.WriteLine(secondTenDates.Any() ? string.Join(", ", secondTenDates.Select(d => d.ToString("yyyy-MM-dd"))) : "No data available.");
Console.WriteLine();

// Fix: Ensure gold price averages are printed for specified years
foreach (var year in new[] { 2020, 2023, 2024 })
{
if (avgPrices.ContainsKey(year))
{
Console.WriteLine($"Averages of gold prices in {year}: {Math.Round(avgPrices[year], 2)}");
}
else
{
Console.WriteLine($"Averages of gold prices in {year}: No data available.");
}
}
Console.WriteLine();

Console.WriteLine("Best investment opportunity:");
Console.WriteLine($" Buy on: {bestTrade.BuyDate:yyyy-MM-dd} at {bestTrade.BuyPrice}");
Console.WriteLine($" Sell on: {bestTrade.SellDate:yyyy-MM-dd} at {bestTrade.SellPrice}");
Console.WriteLine($" Profit: {Math.Round(bestTrade.Profit, 2)}");
Console.WriteLine($" ROI: {Math.Round(bestTrade.ROI, 2)}%\n");


Console.WriteLine("\nGold Analyis Queries with LINQ Completed.");
// gold prices to an XML file
string xmlFilePath = "GoldPrices.xml";
List<GoldPrice> loadedPrices = analysisService.LoadPricesFromXml(xmlFilePath);
Console.WriteLine($"Loaded {loadedPrices.Count} records from XML.");

}
}
1 change: 1 addition & 0 deletions 03-LINQ/GoldSavings.App/TypeScript/RandomList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"use strict";
28 changes: 28 additions & 0 deletions 03-LINQ/GoldSavings.App/TypeScript/RandomList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export class RandomList<T> {
private items: T[] = [];
private random = Math.random;

// Adds an item randomly at the start or end
add(item: T): void {
if (this.random() < 0.5) {
this.items.unshift(item); // Add to beginning
} else {
this.items.push(item); // Add to end
}
}

// Gets a random item up to the specified index
get(maxIndex: number): T {
if (this.items.length === 0) {
throw new Error("List is empty");
}
// Key adapted logic:
const boundedIndex = Math.min(maxIndex, this.items.length - 1);
const randomIndex = Math.floor(this.random() * (boundedIndex + 1));
return this.items[randomIndex];
}

get isEmpty(): boolean {
return this.items.length === 0;
}
}
Loading