diff --git a/83 Stock Market/csharp/Game.csproj b/83 Stock Market/csharp/Game.csproj new file mode 100644 index 00000000..20827042 --- /dev/null +++ b/83 Stock Market/csharp/Game.csproj @@ -0,0 +1,8 @@ + + + + Exe + net5.0 + + + diff --git a/83 Stock Market/csharp/StockMarket.sln b/83 Stock Market/csharp/StockMarket.sln new file mode 100644 index 00000000..5bfb67aa --- /dev/null +++ b/83 Stock Market/csharp/StockMarket.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31321.278 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Game", "Game.csproj", "{BADD262D-D540-431F-8803-2A6F80C22033}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BADD262D-D540-431F-8803-2A6F80C22033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BADD262D-D540-431F-8803-2A6F80C22033}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BADD262D-D540-431F-8803-2A6F80C22033}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BADD262D-D540-431F-8803-2A6F80C22033}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C78DBA4A-87E2-4B31-A261-4AEF5E4C3B12} + EndGlobalSection +EndGlobal diff --git a/83 Stock Market/csharp/src/Assets.cs b/83 Stock Market/csharp/src/Assets.cs new file mode 100644 index 00000000..ecb2eafe --- /dev/null +++ b/83 Stock Market/csharp/src/Assets.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; + +namespace Game +{ + /// + /// Stores the player's assets. + /// + public record Assets + { + /// + /// Gets the player's amount of cash. + /// + public double Cash { get; init; } + + /// + /// Gets the number of stocks owned of each company. + /// + public ImmutableArray Portfolio { get; init; } + } +} diff --git a/83 Stock Market/csharp/src/Broker.cs b/83 Stock Market/csharp/src/Broker.cs new file mode 100644 index 00000000..dc0abb9e --- /dev/null +++ b/83 Stock Market/csharp/src/Broker.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Game +{ + /// + /// Contains functions for exchanging assets. + /// + public static class Broker + { + /// + /// Applies the given set of transactions to the given set of assets. + /// + /// + /// The assets to update. + /// + /// + /// The set of stocks to purchase or sell. Positive values indicate + /// purchaes and negative values indicate sales. + /// + /// + /// The collection of companies. + /// + /// + /// Returns the sellers new assets and a code indicating the result + /// of the transaction. + /// + public static (Assets newAssets, TransactionResult result) Apply(Assets assets, IEnumerable transactions, IEnumerable companies) + { + var (netCost, transactionSize) = Enumerable.Zip( + transactions, + companies, + (amount, company) => (amount * company.SharePrice)) + .Aggregate( + (netCost: 0.0, transactionSize: 0.0), + (accumulated, amount) => (accumulated.netCost + amount, accumulated.transactionSize + Math.Abs(amount))); + + var brokerageFee = 0.01 * transactionSize; + + var newAssets = assets with + { + Cash = assets.Cash - netCost - brokerageFee, + Portfolio = ImmutableArray.CreateRange(Enumerable.Zip( + assets.Portfolio, + transactions, + (sharesOwned, delta) => sharesOwned + delta)) + }; + + if (newAssets.Portfolio.Any(amount => amount < 0)) + return (newAssets, TransactionResult.Oversold); + else + if (newAssets.Cash < 0) + return (newAssets, TransactionResult.Overspent); + else + return (newAssets, TransactionResult.Ok); + } + } +} diff --git a/83 Stock Market/csharp/src/Company.cs b/83 Stock Market/csharp/src/Company.cs new file mode 100644 index 00000000..59f40e8d --- /dev/null +++ b/83 Stock Market/csharp/src/Company.cs @@ -0,0 +1,23 @@ +namespace Game +{ + /// + /// Represents a company. + /// + public record Company + { + /// + /// Gets the company's name. + /// + public string Name { get; init; } + + /// + /// Gets the company's three letter stock symbol. + /// + public string StockSymbol { get; init; } + + /// + /// Gets the company's current share price. + /// + public double SharePrice { get; init; } + } +} diff --git a/83 Stock Market/csharp/src/Controller.cs b/83 Stock Market/csharp/src/Controller.cs new file mode 100644 index 00000000..bccfde0c --- /dev/null +++ b/83 Stock Market/csharp/src/Controller.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Game +{ + public static class Controller + { + /// + /// Manages the initial interaction with the user. + /// + public static void StartGame() + { + View.ShowBanner(); + + var showInstructions = GetYesOrNo(View.PromptShowInstructions); + View.ShowSeparator(); + if (showInstructions) + View.ShowInstructions(); + + View.ShowSeparator(); + } + + /// + /// Gets a yes or no answer from the user. + /// + /// + /// Displays the prompt. + /// + /// + /// True if the user answered yes and false if he or she answered no. + /// + public static bool GetYesOrNo(Action prompt) + { + prompt(); + + var response = default(char); + do + { + response = Console.ReadKey(intercept: true).KeyChar; + } + while (response != '0' && response != '1'); + + View.ShowChar(response); + return response == '1'; + } + + /// + /// Gets a transaction amount for each company in the given collection + /// of companies and returns the updated assets. + /// + /// + /// The assets to update. + /// + /// + /// The collection of companies. + /// + /// + /// The updated assets. + /// + public static Assets UpdateAssets(Assets assets, IEnumerable companies) + { + while (true) + { + View.PromptEnterTransactions(); + + var result = Broker.Apply ( + assets, + companies.Select(GetTransactionAmount).ToList(), + companies); + + switch (result) + { + case (Assets newAssets, TransactionResult.Ok): + return newAssets; + case (_, TransactionResult.Oversold): + View.ShowOversold(); + break; + case (Assets newAssets, TransactionResult.Overspent): + View.ShowOverspent(-newAssets.Cash); + break; + } + } + } + + /// + /// Gets a transaction amount for the given company. + /// + /// + /// The company to buy or sell. + /// + /// + /// The number of shares to buy or sell. + /// + public static int GetTransactionAmount(Company company) + { + while (true) + { + View.PromptBuySellCompany(company); + if (Int32.TryParse(Console.ReadLine(), out var amount)) + return amount; + else + View.PromptValidInteger(); + } + } + } +} diff --git a/83 Stock Market/csharp/src/Extensions/EnumerableExtensions.cs b/83 Stock Market/csharp/src/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..11de50a9 --- /dev/null +++ b/83 Stock Market/csharp/src/Extensions/EnumerableExtensions.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Game.Extensions +{ + /// + /// Provides additional methods for the + /// interface. + /// + public static class EnumerableExtensions + { + /// + /// Simultaneously projects each element of a sequence and applies + /// the result of the previous projection. + /// + /// + /// The type of elements in the source sequence. + /// + /// + /// The type of elements in the result sequence. + /// + /// + /// The source sequence. + /// + /// + /// The seed value for the aggregation component. This value is + /// passed to the first call to . + /// + /// + /// The projection function. This function is supplied with a value + /// from the source sequence and the result of the projection on the + /// previous value in the source sequence. + /// + /// + /// The resulting sequence. + /// + public static IEnumerable SelectAndAggregate( + this IEnumerable source, + TResult seed, + Func selector) + { + foreach (var element in source) + { + seed = selector(element, seed); + yield return seed; + } + } + + /// + /// Combines the results of three distinct sequences into a single + /// sequence. + /// + /// + /// The element type of the first sequence. + /// + /// + /// The element type of the second sequence. + /// + /// + /// The element type of the third sequence. + /// + /// + /// The element type of the resulting sequence. + /// + /// + /// The first source sequence. + /// + /// + /// The second source sequence. + /// + /// + /// The third source sequence. + /// + /// + /// Function that combines results from each source sequence into a + /// final result. + /// + /// + /// A sequence of combined values. + /// + /// + /// + /// This function works identically to Enumerable.Zip except that it + /// combines three sequences instead of two. + /// + /// + /// We have defined this as an extension method for consistency with + /// the similar LINQ methods in the class. + /// However, since there is nothing special about the first sequence, + /// it is often more clear to call this as a regular function. For + /// example: + /// + /// + /// EnumerableExtensions.Zip( + /// sequence1, + /// sequence2, + /// sequence3, + /// (a, b, c) => GetResult (a, b, c)); + /// + /// + public static IEnumerable Zip( + this IEnumerable first, + IEnumerable second, + IEnumerable third, + Func resultSelector) + { + using var enumerator1 = first.GetEnumerator(); + using var enumerator2 = second.GetEnumerator(); + using var enumerator3 = third.GetEnumerator(); + + while (enumerator1.MoveNext() && enumerator2.MoveNext() && enumerator3.MoveNext()) + yield return resultSelector(enumerator1.Current, enumerator2.Current, enumerator3.Current); + } + } +} diff --git a/83 Stock Market/csharp/src/Extensions/RandomExtensions.cs b/83 Stock Market/csharp/src/Extensions/RandomExtensions.cs new file mode 100644 index 00000000..0c3a4230 --- /dev/null +++ b/83 Stock Market/csharp/src/Extensions/RandomExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Game.Extensions +{ + /// + /// Provides additional methods for the class. + /// + public static class RandomExtensions + { + /// + /// Generates an infinite sequence of random numbers. + /// + /// + /// The random number generator. + /// + /// + /// The inclusive lower bound of the range to generate. + /// + /// + /// The exclusive upper bound of the range to generate. + /// + /// + /// An infinite sequence of random integers in the range [min, max). + /// + /// + /// + /// We use an exclusive upper bound, even though it's a little + /// confusing, for the sake of consistency with Random.Next. + /// + /// + /// Since the sequence is infinite, a typical usage would be to cap + /// the results with a function like Enumerable.Take. For example, + /// to sum the results of rolling three six sided dice, we could do: + /// + /// + /// random.Integers(1, 7).Take(3).Sum() + /// + /// + public static IEnumerable Integers(this Random random, int min, int max) + { + while (true) + yield return random.Next(min, max); + } + } +} diff --git a/83 Stock Market/csharp/src/Program.cs b/83 Stock Market/csharp/src/Program.cs new file mode 100644 index 00000000..552d8c9a --- /dev/null +++ b/83 Stock Market/csharp/src/Program.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Game +{ + class Program + { + /// + /// Defines the set of companies that will be simulated in the game. + /// + private static ImmutableArray Companies = ImmutableArray.CreateRange(new[] + { + new Company { Name = "INT. BALLISTIC MISSILES", StockSymbol = "IBM", SharePrice = 100 }, + new Company { Name = "RED CROSS OF AMERICA", StockSymbol = "RCA", SharePrice = 85 }, + new Company { Name = "LICHTENSTEIN, BUMRAP & JOKE", StockSymbol = "LBJ", SharePrice = 150 }, + new Company { Name = "AMERICAN BANKRUPT CO.", StockSymbol = "ABC", SharePrice = 140 }, + new Company { Name = "CENSURED BOOKS STORE", StockSymbol = "CBS", SharePrice = 110 } + }); + + static void Main() + { + var assets = new Assets + { + Cash = 10000.0, + Portfolio = ImmutableArray.CreateRange(Enumerable.Repeat(0, Companies.Length)) + }; + + var previousDay = default(TradingDay); + + Controller.StartGame(); + + foreach (var day in StockMarket.Simulate(Companies)) + { + if (previousDay is null) + View.ShowCompanies(day.Companies); + else + View.ShowTradeResults(day, previousDay, assets); + + View.ShowAssets(assets, day.Companies); + + if (previousDay is not null && !Controller.GetYesOrNo(View.PromptContinue)) + break; + + assets = Controller.UpdateAssets(assets, day.Companies); + previousDay = day; + } + + View.ShowFarewell(); + } + } +} diff --git a/83 Stock Market/csharp/src/StockMarket.cs b/83 Stock Market/csharp/src/StockMarket.cs new file mode 100644 index 00000000..ba2f52ee --- /dev/null +++ b/83 Stock Market/csharp/src/StockMarket.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Game.Extensions; + +namespace Game +{ + /// + /// Provides a method for simulating a stock market. + /// + public static class StockMarket + { + /// + /// Simulates changes in the stock market over time. + /// + /// + /// The collection of companies that will participate in the market. + /// + /// + /// An infinite sequence of trading days. Each day represents the + /// state of the stock market at the start of that day. + /// + public static IEnumerable Simulate(ImmutableArray companies) + { + var random = new Random(); + + var cyclicParameters = EnumerableExtensions.Zip( + Trends(random, 1, 5), + PriceSpikes(random, companies.Length, 1, 5), + PriceSpikes(random, companies.Length, 1, 5), + (trend, company1, company2) => (trend, positiveSpike: company1, negativeSpike: company2)); + + return cyclicParameters.SelectAndAggregate( + new TradingDay + { + Companies = companies + }, + (parameters, previousDay) => previousDay with + { + Companies = ImmutableArray.CreateRange( + previousDay.Companies.Select ((company, index) => AdjustSharePrice( + random, + company, + parameters.trend, + parameters.positiveSpike == index, + parameters.negativeSpike == index))) + }); + } + + /// + /// Creates a copy of a company with a randomly adjusted share price, + /// based on the given parameters. + /// + /// + /// The random number generator. + /// + /// + /// The company to adjust. + /// + /// + /// The slope of the overall market price trend. + /// + /// + /// True if the function should simulate a positive spike in the + /// company's share price. + /// + /// + /// True if the function should simulate a negative spike in the + /// company's share price. + /// + /// + /// The adjusted company. + /// + private static Company AdjustSharePrice(Random random, Company company, double trend, bool positiveSpike, bool negativeSpike) + { + var boost = random.Next(4) * 0.25; + + var spikeAmount = 0.0; + + if (positiveSpike) + spikeAmount = 10; + + if (negativeSpike) + spikeAmount = spikeAmount - 10; + + var priceChange = (int)(trend * company.SharePrice) + boost + (int)(3.5 - (6 * random.NextDouble())) + spikeAmount; + + var newPrice = company.SharePrice + priceChange; + if (newPrice < 0) + newPrice = 0; + + return company with { SharePrice = newPrice }; + } + + /// + /// Generates an infinite sequence of market trends. + /// + /// + /// The random number generator. + /// + /// + /// The minimum number of days each trend should last. + /// + /// + /// The maximum number of days each trend should last. + /// + public static IEnumerable Trends(Random random, int minDays, int maxDays) => + random.Integers(minDays, maxDays + 1).SelectMany(days => Enumerable.Repeat(GenerateTrend(random), days)); + + /// + /// Generates a random value for the market trend. + /// + /// + /// The random number generator. + /// + /// + /// A trend value in the range [-0.1, 0.1]. + /// + private static double GenerateTrend(Random random) => + ((int)(random.NextDouble() * 10 + 0.5) / 100.0) * (random.Next(2) == 0 ? 1 : -1) ; + + /// + /// Generates an infinite sequence of price spikes. + /// + /// + /// The random number generator. + /// + /// + /// The number of companies. + /// + /// + /// The minimum number of days in between price spikes. + /// + /// + /// The maximum number of days in between price spikes. + /// + /// + /// An infinite sequence of random company indexes and null values. + /// A non-null value means that the corresponding company should + /// experience a price spike. + /// + private static IEnumerable PriceSpikes(Random random, int companyCount, int minDays, int maxDays) => + random.Integers(minDays, maxDays + 1) + .SelectMany( + days => Enumerable.Range(0, days), + (days, dayNumber) => dayNumber == 0 ? random.Next(companyCount) : default(int?)); + } +} diff --git a/83 Stock Market/csharp/src/TradingDay.cs b/83 Stock Market/csharp/src/TradingDay.cs new file mode 100644 index 00000000..4e9d93f4 --- /dev/null +++ b/83 Stock Market/csharp/src/TradingDay.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace Game +{ + /// + /// Represents a single trading day. + /// + public record TradingDay + { + /// + /// Gets the average share price of all companies in the market this + /// day. + /// + public double AverageSharePrice => + Companies.Average (company => company.SharePrice); + + /// + /// Gets the collection of public listed companies in the stock market + /// this day. + /// + public ImmutableArray Companies { get; init; } + } +} diff --git a/83 Stock Market/csharp/src/TransactionResult.cs b/83 Stock Market/csharp/src/TransactionResult.cs new file mode 100644 index 00000000..23ee8831 --- /dev/null +++ b/83 Stock Market/csharp/src/TransactionResult.cs @@ -0,0 +1,25 @@ +namespace Game +{ + /// + /// Enumerates the different possible outcomes of applying a transaction. + /// + public enum TransactionResult + { + /// + /// The transaction was successful. + /// + Ok, + + /// + /// The transaction failed because the seller tried to sell more shares + /// than he or she owns. + /// + Oversold, + + /// + /// The transaction failed because the net cost was greater than the + /// seller's available cash. + /// + Overspent + } +} diff --git a/83 Stock Market/csharp/src/View.cs b/83 Stock Market/csharp/src/View.cs new file mode 100644 index 00000000..28426b98 --- /dev/null +++ b/83 Stock Market/csharp/src/View.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Game.Extensions; + +namespace Game +{ + /// + /// Contains functions for displaying information to the user. + /// + public static class View + { + public static void ShowBanner() + { + Console.WriteLine(" STOCK MARKET"); + Console.WriteLine(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine(); + } + + public static void ShowInstructions() + { + Console.WriteLine("THIS PROGRAM PLAYS THE STOCK MARKET. YOU WILL BE GIVEN"); + Console.WriteLine("$10,000 AND MAY BUY OR SELL STOCKS. THE STOCK PRICES WILL"); + Console.WriteLine("BE GENERATED RANDOMLY AND THEREFORE THIS MODEL DOES NOT"); + Console.WriteLine("REPRESENT EXACTLY WHAT HAPPENS ON THE EXCHANGE. A TABLE"); + Console.WriteLine("OF AVAILABLE STOCKS, THEIR PRICES, AND THE NUMBER OF SHARES"); + Console.WriteLine("IN YOUR PORTFOLIO WILL BE PRINTED. FOLLOWING THIS, THE"); + Console.WriteLine("INITIALS OF EACH STOCK WILL BE PRINTED WITH A QUESTION"); + Console.WriteLine("MARK. HERE YOU INDICATE A TRANSACTION. TO BUY A STOCK"); + Console.WriteLine("TYPE +NNN, TO SELL A STOCK TYPE -NNN, WHERE NNN IS THE"); + Console.WriteLine("NUMBER OF SHARES. A BROKERAGE FEE OF 1% WILL BE CHARGED"); + Console.WriteLine("ON ALL TRANSACTIONS. NOTE THAT IF A STOCK'S VALUE DROPS"); + Console.WriteLine("TO ZERO IT MAY REBOUND TO A POSITIVE VALUE AGAIN. YOU"); + Console.WriteLine("HAVE $10,000 TO INVEST. USE INTEGERS FOR ALL YOUR INPUTS."); + Console.WriteLine("(NOTE: TO GET A 'FEEL' FOR THE MARKET RUN FOR AT LEAST"); + Console.WriteLine("10 DAYS)"); + Console.WriteLine("-----GOOD LUCK!-----"); + } + + public static void ShowCompanies(IEnumerable companies) + { + var maxNameLength = companies.Max(company => company.Name.Length); + + Console.WriteLine($"{"STOCK".PadRight(maxNameLength)} INITIALS PRICE/SHARE"); + foreach (var company in companies) + Console.WriteLine($"{company.Name.PadRight(maxNameLength)} {company.StockSymbol} {company.SharePrice:0.00}"); + + Console.WriteLine(); + Console.WriteLine($"NEW YORK STOCK EXCHANGE AVERAGE: {companies.Average(company => company.SharePrice):0.00}"); + Console.WriteLine(); + } + + public static void ShowTradeResults(TradingDay day, TradingDay previousDay, Assets assets) + { + var results = EnumerableExtensions.Zip( + day.Companies, + previousDay.Companies, + assets.Portfolio, + (company, previous, shares) => + ( + stockSymbol: company.StockSymbol, + price: company.SharePrice, + shares, + value: shares * company.SharePrice, + change: company.SharePrice - previous.SharePrice + )).ToList(); + + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("********** END OF DAY'S TRADING **********"); + Console.WriteLine(); + Console.WriteLine(); + + Console.WriteLine("STOCK\tPRICE/SHARE\tHOLDINGS\tVALUE\tNET PRICE CHANGE"); + foreach (var result in results) + Console.WriteLine($"{result.stockSymbol}\t{result.price}\t\t{result.shares}\t\t{result.value:0.00}\t\t{result.change:0.00}"); + + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine(); + + var averagePrice = day.AverageSharePrice; + var averagePriceChange = averagePrice - previousDay.AverageSharePrice; + + Console.WriteLine($"NEW YORK STOCK EXCHANGE AVERAGE: {averagePrice:0.00} NET CHANGE {averagePriceChange:0.00}"); + Console.WriteLine(); + } + + public static void ShowAssets(Assets assets, IEnumerable companies) + { + var totalStockValue = Enumerable.Zip( + assets.Portfolio, + companies, + (shares, company) => shares * company.SharePrice).Sum(); + + Console.WriteLine($"TOTAL STOCK ASSETS ARE ${totalStockValue:0.00}"); + Console.WriteLine($"TOTAL CASH ASSETS ARE ${assets.Cash:0.00}"); + Console.WriteLine($"TOTAL ASSETS ARE ${totalStockValue + assets.Cash:0.00}"); + Console.WriteLine(); + } + + public static void ShowOversold() + { + Console.WriteLine(); + Console.WriteLine("YOU HAVE OVERSOLD A STOCK; TRY AGAIN."); + } + + public static void ShowOverspent(double amount) + { + Console.WriteLine(); + Console.WriteLine($"YOU HAVE USED ${amount:0.00} MORE THAN YOU HAVE."); + } + + public static void ShowFarewell() + { + Console.WriteLine("HOPE YOU HAD FUN!!"); + } + + public static void ShowSeparator() + { + Console.WriteLine(); + Console.WriteLine(); + } + + public static void ShowChar(char c) + { + Console.WriteLine(c); + } + + public static void PromptShowInstructions() + { + Console.Write("DO YOU WANT THE INSTRUCTIONS (YES-TYPE 1, NO-TYPE 0)? "); + } + + public static void PromptContinue() + { + Console.Write("DO YOU WISH TO CONTINUE (YES-TYPE 1, NO-TYPE 0)? "); + } + + public static void PromptEnterTransactions() + { + Console.WriteLine("WHAT IS YOUR TRANSACTION IN"); + } + + public static void PromptBuySellCompany(Company company) + { + Console.Write($"{company.StockSymbol}? "); + } + + public static void PromptValidInteger() + { + Console.WriteLine("PLEASE ENTER A VALID INTEGER"); + } + } +}