diff --git a/Nhl.Api.Common/Attributes/TeamActiveAttribute.cs b/Nhl.Api.Common/Attributes/TeamActiveAttribute.cs new file mode 100644 index 00000000..7de59499 --- /dev/null +++ b/Nhl.Api.Common/Attributes/TeamActiveAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Nhl.Api.Common.Attributes; + +/// +/// An enumeration to specifically mark NHL teams active or inactive +/// +/// +/// The constructor +/// +/// Whether an NHL team is active in the leauge +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public class TeamActiveAttribute(bool isActive) : Attribute +{ + /// + /// Determines if the NHL team is currently active + /// + public bool IsActive { get; } = isActive; +} diff --git a/Nhl.Api.Common/Extensions/EnumExtensions.cs b/Nhl.Api.Common/Extensions/EnumExtensions.cs index 25f6e7f7..0743d80a 100644 --- a/Nhl.Api.Common/Extensions/EnumExtensions.cs +++ b/Nhl.Api.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Reflection; using System.Runtime.Serialization; diff --git a/Nhl.Api.Domain/Enumerations/Team/TeamEnum.cs b/Nhl.Api.Domain/Enumerations/Team/TeamEnum.cs index 3185661b..d2fec817 100644 --- a/Nhl.Api.Domain/Enumerations/Team/TeamEnum.cs +++ b/Nhl.Api.Domain/Enumerations/Team/TeamEnum.cs @@ -1,3 +1,5 @@ +using Nhl.Api.Common.Attributes; + namespace Nhl.Api.Models.Enumerations.Team; /// @@ -8,133 +10,166 @@ public enum TeamEnum /// /// New Jersey Devils /// + [TeamActive(true)] NewJerseyDevils = 1, /// /// New York Islanders /// + [TeamActive(true)] NewYorkIslanders = 2, /// /// New York Rangers /// + [TeamActive(true)] NewYorkRangers = 3, /// /// Philadelphia Flyers /// + [TeamActive(true)] PhiladelphiaFlyers = 4, /// /// Pittsburgh Penguins /// + [TeamActive(true)] PittsburghPenguins = 5, /// /// Boston Bruins /// + [TeamActive(true)] BostonBruins = 6, /// /// Buffalo Sabres /// + [TeamActive(true)] BuffaloSabres = 7, /// /// Montreal Canadiens /// + [TeamActive(true)] MontrealCanadiens = 8, /// /// Ottawa Senators /// + [TeamActive(true)] OttawaSenators = 9, /// /// Toronto Maple Leafs /// + [TeamActive(true)] TorontoMapleLeafs = 10, /// /// Carolina Hurricanes /// + [TeamActive(true)] CarolinaHurricanes = 12, /// /// Florida Panthers /// + [TeamActive(true)] FloridaPanthers = 13, /// /// Tampa Bay Lightning /// + [TeamActive(true)] TampaBayLightning = 14, /// /// Washington Capitals /// + [TeamActive(true)] WashingtonCapitals = 15, /// /// Chicago Blackhawks /// + [TeamActive(true)] ChicagoBlackhawks = 16, /// /// Detroit Red Wings /// + [TeamActive(true)] DetroitRedWings = 17, /// /// Nashville Predators /// + [TeamActive(true)] NashvillePredators = 18, /// /// St.Louis Blues /// + [TeamActive(true)] StLouisBlues = 19, /// /// Calgary Flames /// + [TeamActive(true)] CalgaryFlames = 20, /// /// Colorado Avalanche /// + [TeamActive(true)] ColoradoAvalanche = 21, /// /// Edmonton Oilers /// + [TeamActive(true)] EdmontonOilers = 22, /// /// Vancouver Canucks /// + [TeamActive(true)] VancouverCanucks = 23, /// /// Anaheim Ducks /// + [TeamActive(true)] AnaheimDucks = 24, /// /// Dallas Stars /// + [TeamActive(true)] DallasStars = 25, /// /// Los Angeles Kings /// + [TeamActive(true)] LosAngelesKings = 26, /// /// San Jose Sharks /// + [TeamActive(true)] SanJoseSharks = 28, /// /// Columbus Blue Jackets /// + [TeamActive(true)] ColumbusBlueJackets = 29, /// /// Minnesota Wild /// + [TeamActive(true)] MinnesotaWild = 30, /// /// Winnipeg Jets /// + [TeamActive(true)] WinnipegJets = 52, /// /// Arizona Coyotes /// + [TeamActive(false)] ArizonaCoyotes = 53, /// /// Vegas Golden Knights /// + [TeamActive(true)] VegasGoldenKnights = 54, /// /// Seattle Kraken /// + [TeamActive(true)] SeattleKraken = 55, /// /// Utah Hockey Club /// + [TeamActive(true)] UtahHockeyClub = 59 } diff --git a/Nhl.Api.Domain/Models/Draft/DraftYear.cs b/Nhl.Api.Domain/Models/Draft/DraftYear.cs index 24a67fbd..375d14db 100644 --- a/Nhl.Api.Domain/Models/Draft/DraftYear.cs +++ b/Nhl.Api.Domain/Models/Draft/DraftYear.cs @@ -1,4 +1,4 @@ - + namespace Nhl.Api.Models.Draft; /// diff --git a/Nhl.Api.Domain/Models/Draft/PlayerDraftRanking.cs b/Nhl.Api.Domain/Models/Draft/PlayerDraftRanking.cs new file mode 100644 index 00000000..5384ac70 --- /dev/null +++ b/Nhl.Api.Domain/Models/Draft/PlayerDraftRanking.cs @@ -0,0 +1,160 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Nhl.Api.Models.Draft; + +/// +/// The NHL draft player category +/// +public class PlayerDraftCategory +{ + /// + /// The ID of the player draft category + /// + [JsonProperty("id")] + public int? Id { get; set; } + + /// + /// The name of the player draft category + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The consumer key of the player draft category + /// + [JsonProperty("consumerKey")] + public string ConsumerKey { get; set; } +} + +/// +/// The NHL draft player ranking for each prospective player +/// +public class PlayerDraftRanking +{ + /// + /// The last name of the player + /// + [JsonProperty("lastName")] + public string LastName { get; set; } + + /// + /// The first name of the player + /// + [JsonProperty("firstName")] + public string FirstName { get; set; } + + /// + /// The position code of the player + /// + [JsonProperty("positionCode")] + public string PositionCode { get; set; } + + /// + /// The shooting or catching hand of the player + /// + [JsonProperty("shootsCatches")] + public string ShootsCatches { get; set; } + + /// + /// The height of the player in inches + /// + [JsonProperty("heightInInches")] + public int? HeightInInches { get; set; } + + /// + /// The weight of the player in pounds + /// + [JsonProperty("weightInPounds")] + public int? WeightInPounds { get; set; } + + /// + /// The last amateur club of the player + /// + [JsonProperty("lastAmateurClub")] + public string LastAmateurClub { get; set; } + + /// + /// The last amateur league of the player + /// + [JsonProperty("lastAmateurLeague")] + public string LastAmateurLeague { get; set; } + + /// + /// The birth date of the player + /// + [JsonProperty("birthDate")] + public string BirthDate { get; set; } + + /// + /// The birth city of the player + /// + [JsonProperty("birthCity")] + public string BirthCity { get; set; } + + /// + /// The birth state or province of the player + /// + [JsonProperty("birthStateProvince")] + public string BirthStateProvince { get; set; } + + /// + /// The birth country of the player + /// + [JsonProperty("birthCountry")] + public string BirthCountry { get; set; } + + /// + /// The midterm rank of the player + /// + [JsonProperty("midtermRank")] + public int? MidtermRank { get; set; } + + /// + /// The final rank of the player + /// + [JsonProperty("finalRank")] + public int? FinalRank { get; set; } +} + +/// +/// The NHL draft player draft year with all players and their information and rankings +/// +public class PlayerDraftYear +{ + /// + /// The draft year + /// + [JsonProperty("draftYear")] + public int? DraftYear { get; set; } + + /// + /// The category ID + /// + [JsonProperty("categoryId")] + public int? CategoryId { get; set; } + + /// + /// The category key + /// + [JsonProperty("categoryKey")] + public string? CategoryKey { get; set; } + + /// + /// The list of draft years + /// + [JsonProperty("draftYears")] + public List DraftYears { get; set; } = []; + + /// + /// The list of player draft categories + /// + [JsonProperty("categories")] + public List Categories { get; set; } = []; + + /// + /// The list of player draft rankings + /// + [JsonProperty("rankings")] + public List Rankings { get; set; } = []; +} diff --git a/Nhl.Api.Domain/Models/Draft/Ranks.cs b/Nhl.Api.Domain/Models/Draft/Ranks.cs deleted file mode 100644 index ea4bce13..00000000 --- a/Nhl.Api.Domain/Models/Draft/Ranks.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace Nhl.Api.Models.Draft; -/// -/// NHL Prospect Ranks -/// -public class Ranks -{ - /// - /// The final rank for the NHL prospect
- /// Example: 1 - ///
- [JsonProperty("finalRank")] - public int FinalRank { get; set; } - - /// - /// The draft year for the NHL prospect
- /// Example: 2021 - ///
- [JsonProperty("draftYear")] - public int DraftYear { get; set; } -} diff --git a/Nhl.Api.Domain/Models/Game/GameCenterPlayByPlay.cs b/Nhl.Api.Domain/Models/Game/GameCenterPlayByPlay.cs index 63a725bb..14641d84 100644 --- a/Nhl.Api.Domain/Models/Game/GameCenterPlayByPlay.cs +++ b/Nhl.Api.Domain/Models/Game/GameCenterPlayByPlay.cs @@ -365,10 +365,16 @@ public class GameCenterPlay public GameCenterDetails Details { get; set; } /// - /// The estimated time of the play for the NHL game center play by play, these are not exact times and are close approximations of each game event
+ /// The estimated time of the play for the NHL game center play by play, these are not exact times and are close approximations of each game event within 1 (one) hour of the event occurring
+ /// this is due to some factors which include but are not limited to:
+ /// + /// An active NHL game currently in progress + /// No score sheet or report available for correct start and end times of periods + /// No public recordings of the end time of the game/period + /// /// Example: 2024-01-13T20:12:23Z ///
- public DateTimeOffset? EstimatedDateTimeOfPlay { get; set; } + public DateTimeOffset? EstimatedDateTimeOfPlay { get; set; } = null; } /// diff --git a/Nhl.Api.Tests/GameTests.cs b/Nhl.Api.Tests/GameTests.cs index 58c99259..9666b092 100644 --- a/Nhl.Api.Tests/GameTests.cs +++ b/Nhl.Api.Tests/GameTests.cs @@ -208,48 +208,45 @@ public async Task GetGameMetadataByGameIdAsync_Return_Valid_Information(int game } [TestMethodWithRetry(RetryCount = 5)] - public async Task NhlScoresHtmlReportsApiHttpClient_Can_Parse_Html_Report_For_Start_End_Period_Times() + [DataRow(2000020004)] + [DataRow(2001020004)] + [DataRow(2002020004)] + [DataRow(2003020004)] + [DataRow(2005020037)] + [DataRow(2006020046)] + [DataRow(2007020055)] + [DataRow(2008020074)] + [DataRow(2009020090)] + [DataRow(2010020090)] + [DataRow(2011020090)] + [DataRow(2012020090)] + [DataRow(2013020090)] + [DataRow(2014020090)] + [DataRow(2015020090)] + [DataRow(2016020090)] + [DataRow(2017020090)] + [DataRow(2018020090)] + [DataRow(2023010110)] + [DataRow(2019020090)] + [DataRow(2020020090)] + [DataRow(2021020090)] + [DataRow(2022020090)] + [DataRow(2023020090)] + public async Task GetGameCenterPlayByPlayByGameIdAsync_Returns_Valid_Estimated_DateTime_Of_Play_Information(int gameId) { - var dictionary = new Dictionary> - { - { "P1", new List() }, - { "P2", new List() }, - { "P3", new List() }, - { "OT", new List() }, - { "SH", new List() }, - }; - - var httpClient = new NhlScoresHtmlReportsApiHttpClient(); - var gameReport = await httpClient.GetStringAsync("/20232024/PL020206.HTM"); - - var regex = Regex.Matches(gameReport, @"(?<=)Period(.*?)(?=)", RegexOptions.Compiled, TimeSpan.FromSeconds(30)).ToList(); - - for (var i = 0; i < regex.Count; i++) - { - var match = regex[i].Value; - var value = Regex.Match(match, @"([0-9]{1,2}:[0-9]{2})", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(30)).Groups[0].Value; - var time = TimeOnly.Parse($"{value} PM"); - - if (i <= 1) - { - dictionary["P1"].Add(time); - } - else if (i >= 2 && i <= 3) - { - dictionary["P2"].Add(time); - } - else if (i >= 4 && i <= 5) - { - dictionary["P3"].Add(time); - } - else if (i >= 6 && i <= 7) - { - dictionary["OT"].Add(time); - } - else if (i <= 9) - { - dictionary["SH"].Add(time); - } - } + // Arrange + await using var nhlApi = new NhlApi(); + + // Act + var results = await nhlApi.GetGameCenterPlayByPlayByGameIdAsync(gameId, includeEventDateTime: true); + + // Assert + Assert.IsNotNull(results); + Assert.IsNotNull(results.GameDate); + Assert.IsNotNull(results.GameType); + Assert.IsNotNull(results.Id); + Assert.IsNotNull(results.Clock); + Assert.IsNotNull(results.Plays); + Assert.IsTrue(results.Plays.All(p => p.EstimatedDateTimeOfPlay.HasValue)); } } diff --git a/Nhl.Api.Tests/PlayerTests.cs b/Nhl.Api.Tests/PlayerTests.cs index 78db2432..9a1b8156 100644 --- a/Nhl.Api.Tests/PlayerTests.cs +++ b/Nhl.Api.Tests/PlayerTests.cs @@ -31,7 +31,7 @@ public async Task SearchAllPlayersAsync_Returns_Valid_Results(string query, int Assert.AreEqual("Brantford", playerSearchResult.BirthCity); Assert.AreEqual("CAN", playerSearchResult.BirthCountry); Assert.AreEqual("Canada", playerSearchResult.FullBirthCountry); - Assert.AreEqual("Ontario", playerSearchResult.BirthProvinceState); + Assert.AreEqual("ON", playerSearchResult.BirthProvinceState); Assert.AreEqual("Wayne", playerSearchResult.FirstName); Assert.AreEqual("Gretzky", playerSearchResult.LastName); Assert.AreEqual("NYR", playerSearchResult.LastTeamAbbreviation); @@ -57,7 +57,7 @@ public async Task SearchAllPlayersAsync_Returns_Valid_Results(string query, int Assert.AreEqual("Richmond Hill", playerSearchResult.BirthCity); Assert.AreEqual("CAN", playerSearchResult.BirthCountry); Assert.AreEqual("Canada", playerSearchResult.FullBirthCountry); - Assert.AreEqual("Ontario", playerSearchResult.BirthProvinceState); + Assert.AreEqual("ON", playerSearchResult.BirthProvinceState); Assert.AreEqual("Connor", playerSearchResult.FirstName); Assert.AreEqual("McDavid", playerSearchResult.LastName); Assert.AreEqual(true, playerSearchResult.IsActive); @@ -65,6 +65,9 @@ public async Task SearchAllPlayersAsync_Returns_Valid_Results(string query, int Assert.AreEqual("6\u00271\"", playerSearchResult.Height); Assert.AreEqual(97, playerSearchResult.PlayerNumber); break; + + default: + break; } } @@ -121,7 +124,7 @@ public async Task SearchAllActivePlayersAsync_Returns_Valid_Information(string q Assert.AreEqual("Richmond Hill", playerSearchResult.BirthCity); Assert.AreEqual("CAN", playerSearchResult.BirthCountry); Assert.AreEqual("Canada", playerSearchResult.FullBirthCountry); - Assert.AreEqual("Ontario", playerSearchResult.BirthProvinceState); + Assert.AreEqual("ON", playerSearchResult.BirthProvinceState); Assert.AreEqual("Connor", playerSearchResult.FirstName); Assert.AreEqual("McDavid", playerSearchResult.LastName); Assert.AreEqual(true, playerSearchResult.IsActive); @@ -879,6 +882,47 @@ public async Task GetAllPlayersAsync_Returns_All_Players() Assert.IsTrue(players.Count > 22000); } + [TestMethodWithRetry(RetryCount = 5)] + [DataRow("2008")] + [DataRow("2009")] + [DataRow("2010")] + [DataRow("2011")] + [DataRow("2012")] + [DataRow("2013")] + [DataRow("2014")] + [DataRow("2015")] + [DataRow("2016")] + [DataRow("2017")] + [DataRow("2018")] + [DataRow("2019")] + [DataRow("2020")] + [DataRow("2021")] + [DataRow("2022")] + [DataRow("2023")] + [DataRow("2024")] + public async Task GetPlayerDraftRankingByYearAsync_Returns_Correct_Draft_Ranking_Information(string seasonYear) + { + // Arrange + await using var nhlApi = new NhlApi(); + + // Act + var draft = await nhlApi.GetPlayerDraftRankingByYearAsync(seasonYear); + + // Assert + Assert.IsNotNull(draft); + Assert.IsNotNull(draft.Rankings); + Assert.IsTrue(draft.Rankings.Count > 0); + + foreach (var playerDraftRanking in draft.Rankings) + { + Assert.IsNotNull(playerDraftRanking); + Assert.IsNotNull(playerDraftRanking.FirstName); + Assert.IsNotNull(playerDraftRanking.LastName); + Assert.IsNotNull(playerDraftRanking.HeightInInches); + Assert.IsNotNull(playerDraftRanking.WeightInPounds); + } + + } [TestMethodWithRetry(RetryCount = 25)] public async Task PlayerEnumFileGeneratorHelper_Returns_Valid_Content() => diff --git a/Nhl.Api/Src/Api/NhlApi.cs b/Nhl.Api/Src/Api/NhlApi.cs index d2e67043..77dcff57 100644 --- a/Nhl.Api/Src/Api/NhlApi.cs +++ b/Nhl.Api/Src/Api/NhlApi.cs @@ -1,3 +1,5 @@ +using Nhl.Api.Models.Draft; + namespace Nhl.Api; /// @@ -719,6 +721,17 @@ public async Task GetGoalieStatisticsBySeasonAndFi /// Returns the NHL game direct box score including information such as summaries, linescores, shots by period and more public async Task GetBoxscoreByGameIdAsync(int gameId, CancellationToken cancellationToken = default) => await _nhlGameApi.GetBoxscoreByGameIdAsync(gameId, cancellationToken); + + /// + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + /// + /// The NHL draft year + /// The starting position of the NHL draft by the year + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + public async Task GetPlayerDraftRankingByYearAsync(string seasonYear, int startingPosition = 1, CancellationToken cancellationToken = default) => + await _nhlPlayerApi.GetPlayerDraftRankingByYearAsync(seasonYear, startingPosition, cancellationToken); + /// /// Releases and disposes all unused or garbage collected resources for the Nhl.Api /// diff --git a/Nhl.Api/Src/PlayerApi/INhlPlayerApi.cs b/Nhl.Api/Src/PlayerApi/INhlPlayerApi.cs index 61cbc489..2ecccad2 100644 --- a/Nhl.Api/Src/PlayerApi/INhlPlayerApi.cs +++ b/Nhl.Api/Src/PlayerApi/INhlPlayerApi.cs @@ -1,4 +1,6 @@ -namespace Nhl.Api; +using Nhl.Api.Models.Draft; + +namespace Nhl.Api; /// /// The official unofficial NHL Player API providing various NHL information about players, draft prospects, rosters and more @@ -128,4 +130,13 @@ public interface INhlPlayerApi : IDisposable /// Returns all the NHL players to ever play in the NHL public Task> GetAllPlayersAsync(CancellationToken cancellationToken = default); + /// + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + /// + /// The NHL draft year + /// The starting position of the NHL draft by the year + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + public Task GetPlayerDraftRankingByYearAsync(string seasonYear, int startingPosition = 1, CancellationToken cancellationToken = default); + } diff --git a/Nhl.Api/Src/PlayerApi/NhlPlayerApi.cs b/Nhl.Api/Src/PlayerApi/NhlPlayerApi.cs index 6389728a..539516fc 100644 --- a/Nhl.Api/Src/PlayerApi/NhlPlayerApi.cs +++ b/Nhl.Api/Src/PlayerApi/NhlPlayerApi.cs @@ -1,4 +1,5 @@ using Nhl.Api.Common.Services; +using Nhl.Api.Models.Draft; using Nhl.Api.Services; namespace Nhl.Api; @@ -96,7 +97,7 @@ public async Task GetPlayerHeadshotImageAsync(int playerId, string seaso throw new ArgumentException($"The {nameof(seasonYear)} parameter must be in the format of yyyyyyyy, example: 20232024", nameof(seasonYear)); } - var playerInformation = await GetPlayerInformationAsync(playerId, cancellationToken); + var playerInformation = await this.GetPlayerInformationAsync(playerId, cancellationToken); var teamName = playerInformation.SeasonTotals.FirstOrDefault(x => x.Season == int.Parse(seasonYear))?.TeamName?.Default; if (string.IsNullOrWhiteSpace(teamName)) { @@ -282,10 +283,32 @@ public async Task> GetAllPlayersAsync(CancellationT return (await Task.WhenAll(playerSearchResultsTasks)).SelectMany(playerData => playerData.Data).ToList(); } + /// + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + /// + /// The NHL draft year + /// The starting position of the NHL draft by the year + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation + /// Returns the NHL draft ranking by the specified year and starting position for the draft year + public async Task GetPlayerDraftRankingByYearAsync(string seasonYear, int startingPosition = 1, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(seasonYear)) + { + throw new ArgumentException($"The {nameof(seasonYear)} parameter is required", nameof(seasonYear)); + } + + if (startingPosition < 1) + { + throw new ArgumentException($"The {nameof(startingPosition)} parameter must be greater than 0", nameof(startingPosition)); + } + + return await _nhlApiWebHttpClient.GetAsync($"/draft/rankings/{seasonYear}/{startingPosition}", cancellationToken); + + } + /// /// Disposes and releases all unneeded resources for the NHL player API /// - /// public void Dispose() { _cachingService?.Dispose(); diff --git a/Nhl.Api/Src/StatisticsApi/NhlStatisticsApi.cs b/Nhl.Api/Src/StatisticsApi/NhlStatisticsApi.cs index 47a3cdb0..fee0413c 100644 --- a/Nhl.Api/Src/StatisticsApi/NhlStatisticsApi.cs +++ b/Nhl.Api/Src/StatisticsApi/NhlStatisticsApi.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Nhl.Api.Services; namespace Nhl.Api; @@ -459,13 +460,16 @@ public async Task().ToList(); - var teamRosterTasks = allNhlTeams.Select(async (team) => + // Get all NHL Teams + if (int.Parse(seasonYear, CultureInfo.InvariantCulture) >= int.Parse(SeasonYear.season20242025, CultureInfo.InvariantCulture)) { - return await _nhlLeagueApi.GetTeamRosterBySeasonYearAsync(team, seasonYear, cancellationToken); - }); + // They are no longer in the NHL + allNhlTeams.Remove(TeamEnum.ArizonaCoyotes); + } + + var teamRosterTasks = allNhlTeams.Select(async (team) => await _nhlLeagueApi.GetTeamRosterBySeasonYearAsync(team, seasonYear, cancellationToken)); // Get all team rosters var allRoster = await Task.WhenAll(teamRosterTasks);