public Source Load() { Debug.WriteLine("Loading " + tsvFile); string[] text = File.ReadAllLines(tsvFile); if (text.Length < 1) { throw new ArgumentException("TSV does not have any data. Check format of spreadsheet."); } // Build a map of the incoming data string headerRow = text[0]; string[] columns = headerRow.Split('\t').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); int numberOfHeaders = columns.Length; PropertyEnum[] resolved = new PropertyEnum[numberOfHeaders]; for (int i = 0; i < numberOfHeaders; ++i) { string header = columns[i].ToLowerInvariant() .Replace("your ", "") .Replace(" ", "") .Replace("_", "") .Replace(":", "") .Replace("-", "") .Replace("'s", "") .Replace("teamcaptain", "captain") .Replace("teammember", "player") ; if (string.IsNullOrWhiteSpace(header)) { Console.WriteLine($"Warning: Unable to resolve header, it is blank after processing: \"{header}\", (was \"{columns[i]}\")"); resolved[i] = PropertyEnum.UNKNOWN; } // Quick case else if (propertyValueStringMap.ContainsKey(header)) { resolved[i] = propertyValueStringMap[header]; } else { // Need to do some searching int playerNum = 0; if (header.Contains("captain")) { playerNum = 1; header = header.Replace("captain", ""); } else { for (playerNum = 10; playerNum > 0; --playerNum) { if (header.Contains(playerNum.ToString())) { header = header.Replace(playerNum.ToString(), ""); break; } } } var matchedKey = propertyValueStringMap.Keys.FirstOrDefault(key => header.StartsWith(key)); if (matchedKey != null) { // For player num = 0 (not found), this will just return the appropriate header. resolved[i] = (playerNum * (int)PropertyEnum.PlayerN_Offset) + propertyValueStringMap[matchedKey]; } else if (playerNum == 0) { resolved[i] = PropertyEnum.UNKNOWN; Console.WriteLine("Warning: Unable to resolve header: " + header); } else { resolved[i] = PropertyEnum.UNKNOWN; Console.WriteLine("Warning: Unable to resolve header for player " + playerNum + ": " + header); } } } // Now read the data... // Most tsv files will be team sheets, but for draft cups and verif they could be player records instead. // Each row therefore might be a single team with players, or a single player entry. List <Team> teams = new List <Team>(); List <Player> players = new List <Player>(); // From 1 as [0] is header row for (int lineIndex = 1; lineIndex < text.Length; ++lineIndex) { string line = text[lineIndex]; if (!line.Contains('\t')) { continue; } string[] cells = line.Split('\t').ToArray(); // Warn if the values exceeds the number of defined headers (but don't bother if we're only one over and it's empty -- trailing tab) if (numberOfHeaders < cells.Length && (numberOfHeaders != cells.Length - 1 || !string.IsNullOrWhiteSpace(cells[cells.Length - 1]))) { Console.WriteLine($"Warning: Line {lineIndex} contains more cells in this row {cells.Length} than headers {numberOfHeaders}."); Debug.WriteLine(line); } SortedDictionary <int, Player> rowPlayers = new SortedDictionary <int, Player>(); Team t = new Team(); for (int i = 0; i < cells.Length && i < numberOfHeaders; ++i) { int playerNum = 0; PropertyEnum resolvedProperty = resolved[i]; if (resolvedProperty > PropertyEnum.PlayerN_Offset) { playerNum = (int)resolved[i] / (int)PropertyEnum.PlayerN_Offset; resolvedProperty = (PropertyEnum)((int)resolved[i] % (int)PropertyEnum.PlayerN_Offset); } string value = cells[i]; if (string.IsNullOrWhiteSpace(value)) { continue; } switch (resolvedProperty) { case PropertyEnum.UNKNOWN: { continue; } case PropertyEnum.Country: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); p.Country = value; break; } case PropertyEnum.DiscordId: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); if (value.Length >= 14 && value.Length < 21) { // First, test is decimal (of length 17+) bool isDecimal = value.Length >= 17 && value.All("0123456789".Contains); if (isDecimal) { p.AddDiscordId(value, source); } // Otherwise test if we can get from a hex string (of length 14+ to give the correct 17 digit decimal) else if (ulong.TryParse(value, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out ulong parsedId)) { p.AddDiscordId(value, source); } else { Console.WriteLine($"Warning: DiscordId was specified ({lineIndex},{i}), but the value could not be parsed from a decimal or hex string. {value}."); Debug.WriteLine(line); } } else { Console.WriteLine($"Warning: DiscordId was specified ({lineIndex},{i}), but the value length does not fit into a discord id. {value}."); Debug.WriteLine(line); } break; } case PropertyEnum.DiscordName: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); if (Discord.DISCORD_NAME_REGEX.IsMatch(value)) { p.AddDiscordUsername(value, source); } else if (FriendCode.TryParse(value, out FriendCode friendCode)) { p.AddFCs(friendCode, source); Console.WriteLine($"Warning: This value was declared as a Discord name but looks like a friend code. Bad data formatting? {value} on ({lineIndex},{i})."); } else { Console.WriteLine($"Warning: DiscordName was specified ({lineIndex},{i}), but the value was not in a Discord format of name#0000. {value}."); Debug.WriteLine(line); } break; } case PropertyEnum.FC: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); if (FriendCode.TryParse(value, out FriendCode friendCode)) { p.AddFCs(friendCode, source); } else { Console.WriteLine($"Warning: FC was specified ({lineIndex},{i}), but the value was not in an FC format of 0000-0000-0000 or 0000 0000 0000. {value}."); Debug.WriteLine(line); } break; } case PropertyEnum.Div: { t.AddDivision(new Division(value, divType, season), source); if (divType == DivType.Unknown) { Console.WriteLine($"Warning: Div was specified ({lineIndex},{i}), but I don't know what type of division this file represents."); } break; } case PropertyEnum.LUTIDiv: { t.AddDivision(new Division(value, DivType.LUTI, season), source); break; } case PropertyEnum.EBTVDiv: { t.AddDivision(new Division(value, DivType.EBTV, season), source); break; } case PropertyEnum.DSBDiv: { t.AddDivision(new Division(value, DivType.DSB, season), source); break; } case PropertyEnum.Timestamp: { // TODO - not supported right now. In future we could customise the source timestamp for this entry. break; } case PropertyEnum.Name: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); var playerName = value.Trim(); playerName = t.Tag?.StripFromPlayer(playerName) ?? playerName; p.AddName(playerName, source); if (FriendCode.TryParse(value, out FriendCode friendCode)) { p.AddFCs(friendCode, source); Console.WriteLine($"Warning: This value was declared as a name but looks like a friend code. Bad data formatting? {value} on ({lineIndex},{i})."); Debug.WriteLine(line); } break; } case PropertyEnum.Role: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); p.AddWeapons(value.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrWhiteSpace(s))); break; } case PropertyEnum.Pronouns: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); p.PronounInformation.SetPronoun(value, source); break; } case PropertyEnum.Tag: { t.AddClanTag(value, source); break; } case PropertyEnum.TeamName: { t.AddName(value, source); break; } case PropertyEnum.Twitter: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); p.AddTwitter(value, source); break; } case PropertyEnum.Twitch: { var p = GetCurrentPlayer(ref rowPlayers, playerNum, tsvFile); p.AddTwitch(value, source); break; } case PropertyEnum.Team_Offset: case PropertyEnum.UnspecifiedPlayer_Offset: case PropertyEnum.PlayerN_Offset: default: { Console.WriteLine($"Warning: Unhandled header {resolvedProperty}. {value}. Line {lineIndex}:"); Debug.WriteLine(line); break; } } } // End of the row, add the data. foreach (var pair in rowPlayers) { // Don't add empty players Player p = pair.Value; if (p.Name.Equals(Builtins.UnknownPlayerName)) { continue; } // Don't add the team to the player if it's not complete (e.g. single player record) if (!t.Name.Equals(Builtins.UnknownTeamName)) { p.TeamInformation.Add(t.Id, source); } players.Add(p); } // Don't bother adding the team if it has no players if (players.Count > 0) { // Don't register a team if that information doesn't exist. if (!t.Name.Equals(Builtins.UnknownTeamName)) { // Recalculate the ClanTag layout if (t.Tag != null) { t.Tag.CalculateTagOption(players[0].Name.Value); } else { ClanTag?newTag = ClanTag.CalculateTagFromNames(players.Select(p => p.Name.Value).ToArray(), source); if (newTag != null) { t.AddClanTag(newTag); } } teams.Add(t); } } } source.Players = players.ToArray(); source.Teams = teams.ToArray(); return(source); }
public void SerializeTeam() { var sources = new Dictionary <string, Source>(); var oldestSource = new Source("old_source", DateTime.Now.AddDays(-6)); var newestSource = new Source("new_source", DateTime.Now); sources.Add(newestSource.Id, newestSource); sources.Add(oldestSource.Id, oldestSource); Team team = new Team(); team.AddBattlefyId("2teamid2", oldestSource); team.AddBattlefyId("1teamid1", newestSource); team.AddClanTag("old", oldestSource); team.AddClanTag("new", newestSource); team.AddDivision(new Division(10, DivType.DSB, "S9"), oldestSource); team.AddDivision(new Division(8, DivType.LUTI, "SX"), newestSource); team.AddName("team2", oldestSource); team.AddName("team1", newestSource); string json = Serialize(team); Console.WriteLine(nameof(SerializeTeam) + ": "); Console.WriteLine(json); Team deserialized = Deserialize <Team>(json, sources); var battlefy = deserialized.BattlefyPersistentTeamIdInformation.GetItemsOrdered(); Assert.AreEqual(2, battlefy.Count, "Unexpected number of team battlefy slugs"); Assert.AreEqual("1teamid1", battlefy[0].Value, "Slug [0] unexpected handle"); Assert.AreEqual("2teamid2", battlefy[1].Value, "Slug [1] unexpected handle"); Assert.AreEqual("new_source", battlefy[0].Sources[0].Name, "Slug [0] unexpected source"); Assert.AreEqual("old_source", battlefy[1].Sources[0].Name, "Slug [1] unexpected source"); var clanTags = deserialized.ClanTagInformation.GetItemsOrdered(); Assert.AreEqual(2, clanTags.Count, "Unexpected number of clanTags"); Assert.AreEqual("new", clanTags[0].Value, "clanTags[0] Unexpected Value"); Assert.AreEqual("old", clanTags[1].Value, "clanTags[1] Unexpected Value"); Assert.AreEqual("new_source", clanTags[0].Sources[0].Name, "clanTags[0] unexpected source"); Assert.AreEqual("old_source", clanTags[1].Sources[0].Name, "clanTags[1] unexpected source"); var divisions = deserialized.DivisionInformation.GetDivisionsOrdered(); Assert.AreEqual(2, divisions.Count, "Unexpected number of divisions"); // First - most recent Assert.AreEqual(8, divisions[0].Value, "Unexpected Value"); Assert.AreEqual(DivType.LUTI, divisions[0].DivType, "Unexpected DivType"); Assert.AreEqual("SX", divisions[0].Season, "Unexpected Season"); // Next Assert.AreEqual(10, divisions[1].Value, "Unexpected Value"); Assert.AreEqual(DivType.DSB, divisions[1].DivType, "Unexpected DivType"); Assert.AreEqual("S9", divisions[1].Season, "Unexpected Season"); var names = deserialized.NamesInformation.GetItemsOrdered(); Assert.AreEqual(2, names.Count, "Unexpected number of team names"); Assert.AreEqual("team1", names[0].Value, "Names [0] unexpected handle"); Assert.AreEqual("team2", names[1].Value, "Names [1] unexpected handle"); Assert.AreEqual("new_source", names[0].Sources[0].Name, "Names [0] unexpected source"); Assert.AreEqual("old_source", names[1].Sources[0].Name, "Names [1] unexpected source"); }