Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text;
- // The sole author of this program is https://puzzling.stackexchange.com/users/12582/steve
- // Supplied "as is". No support given, and no implied suitability for any purpose.
- // It was intended for my own use, and was heavily adapted during the time the puzzle was live
- // This is the version as of the final "(Conclusion)" update (with the following comment blocks added)
- // This is the program that was used behind the scenes for my puzzle at
- // https://puzzling.stackexchange.com/questions/100512/hacked-maps-back-in-the-uk
- // If you somehow found this program before the puzzle, you already know
- // the main answer just by seeing the filename and class names...
- // It runs on the command line, so map output is via a URL to copy-paste into your browser.
- // Expects to find a copy of RailReferences.csv in the same folder for initialisation.
- // (if you don't know what that is or where to get it, this program probably isn't for you yet!)
- // This program (together with appropriate third-party websites) takes the role of:
- // - the "hacked maps" system itself.
- // - the "companion program" that looks up keywords
- // - the program that "computer guy" made for me after the puzzle was solved.
- // Input of the following forms is recognised:
- // CODE
- // - outputs full details of the specified code and any codes from which it was constructed.
- // or full details of any "error code".
- // filename.txt
- // - crashes with an exception if the file doesn't exist.
- // otherwise, treats each line of the file as a code.
- // Use this with dictionaries and other word lists.
- // - outputs details of any valid codes found, and then a classification by error code
- // - finally a URL that allows all of the valid codes to be viewed on a map.
- // filename.txt,N
- // where N is a number from 2-9
- // - ignores any lines in the file not of the specified length.
- // - allows for quickly scanning a large dictionary and finding (e.g.) all valid 7-letter keywords.
- // - accented characters are converted to non-accented versions.
- // - non-alphabetic characters are ignored.
- // - '?' can be used as a wildcard character,
- // and is expanded to a series of 26 strings replacing it with each of 'A' to 'Z'
- // filename.txt,CODE
- // filters the file to include only lines containing the specified CODE as a substring
- // filename.txt,*
- // searches specifically for 6-letter keywords consisting of 3 two-letter codes.
- // CODEA,CODEB,CODEC
- // same handling as filename.txt
- // also used for a single wild-carded code.
- // Eastings,Northings
- // finds valid keywords within a default radius of 50m
- // (for 3-codes, only valid keywords of 3 consecutive distances are considered)
- // In addition, the closest 3 valid keywords from the most recently parsed dictionary are shown.
- // A known bug that I can't be bothered to fix right now is that some keywords outside a 25m radius
- // may be missed, as they have more than a 50m difference between stations in opposite directions.
- // Eastings,Northings,Radius
- // Eastings,Northings,Radius,Option
- // finds valid keywords within a specified radius
- // Option 1 (default) finds all codes
- // Option 0 ignores 3 part codes, searching only for 2 part codes and dictionary entries.
- // Option 2-9 : finds only keywords of the specified length.
- // GridRef
- // GridRef,Radius
- // GridRef,Radius,Option
- // same as for Eastings,Northings, but specified via an OS grid reference (with no spaces)
- public interface IGridReference
- {
- double X { get; }
- double Y { get; }
- }
- public interface IStationCode : IGridReference
- {
- string Code { get; }
- string Name { get; }
- string FullInfo { get; }
- }
- public enum InvalidCodeReason
- {
- Ambiguous,
- NotFound,
- OutsideArea,
- TooLong,
- }
- public class InvalidCodeSentinel : IStationCode
- {
- public InvalidCodeSentinel(InvalidCodeReason reason, string message)
- {
- Reason = reason;
- Message = message;
- }
- public InvalidCodeReason Reason { get; }
- public string Message { get; }
- public double X => double.NaN;
- public double Y => double.NaN;
- public string Code => Reason.ToString();
- public string Name => string.IsNullOrEmpty(Message) ? Code : Message;
- public string FullInfo => string.IsNullOrEmpty(Message) ? Code : ToString();
- public override string ToString() => $"{Code}: {Name}";
- private static Dictionary<InvalidCodeReason, InvalidCodeSentinel> DefaultSentinelsDict = new Dictionary<InvalidCodeReason, InvalidCodeSentinel>();
- public static implicit operator InvalidCodeSentinel(InvalidCodeReason reason)
- {
- if (!DefaultSentinelsDict.TryGetValue(reason, out var result))
- DefaultSentinelsDict.Add(reason, result = new InvalidCodeSentinel(reason, null));
- return result;
- }
- }
- public class StationCode : IStationCode
- {
- public static void ParseLine(string line, Action<int, string> fieldAction)
- {
- int lastIndex = 0;
- int field = 0;
- while (line.IndexOf(',', lastIndex + 1) is int nextIndex && nextIndex > -1)
- {
- string text = line.Substring(lastIndex + 1, nextIndex - lastIndex - 1);
- fieldAction(field, text);
- field++;
- lastIndex = nextIndex;
- }
- }
- enum FieldNames
- {
- Unknown,
- CrsCode,
- StationName,
- Easting,
- Northing,
- }
- static FieldNames[] Fields = new FieldNames[16];
- public static void InitFields(string fieldNamesLine)
- {
- ParseLine(fieldNamesLine, (i, text) => Fields[i] = Enum.TryParse<FieldNames>(text.Trim('\"'), out var name) ? name : FieldNames.Unknown);
- }
- public StationCode(string line)
- {
- string[] TextsToTrim = new[] { " Rail Station", " (Rail Station)", " Railway Station" };
- void initField(int fieldIndex, string text)
- {
- switch (Fields[fieldIndex])
- {
- case FieldNames.CrsCode: Code = text.Trim('\"'); break;
- case FieldNames.StationName:
- Name = text.Trim('\"');
- string textToTrim = TextsToTrim.FirstOrDefault(Name.EndsWith);
- if (textToTrim == null)
- Console.WriteLine("What should I trim from: " + text);
- else
- Name = Name.Substring(0, Name.Length - textToTrim.Length);
- break;
- case FieldNames.Easting: X = Convert.ToDouble(text); break;
- case FieldNames.Northing: Y = Convert.ToDouble(text); break;
- }
- }
- ParseLine(line, initField);
- }
- public string Code { get; private set; }
- public string Name { get; private set; }
- public string FullInfo => ToString();
- public double X { get; private set; }
- public double Y { get; private set; }
- public override string ToString()
- {
- return $"{this.GetGridRef()}({X},{Y}): {Code} {Name}";
- }
- }
- public static class StationCodeExtension
- {
- public static double GetDist2(this IStationCode self, IStationCode other)
- {
- return (self.X - other.X) * (self.X - other.X) + (self.Y - other.Y) * (self.Y - other.Y);
- }
- public static double GetDist2(this IStationCode self, double otherX, double otherY)
- {
- return (self.X - otherX) * (self.X - otherX) + (self.Y - otherY) * (self.Y - otherY);
- }
- private const string Letters = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
- public static string GetGridRefLetters(this IGridReference self)
- {
- int bigSquareX = 2 + (int)Math.Floor(self.X / 500000);
- int bigSquareY = 3 - (int)Math.Floor(self.Y / 500000);
- if (bigSquareX < 0 || bigSquareX >= 5 || bigSquareY < 0 || bigSquareY >= 5)
- return "##";
- int smallSquareX = (int)Math.Floor(((self.X + 1000000) % 500000) / 100000);
- int smallSquareY = 4 - (int)Math.Floor(((self.Y + 1000000) % 500000) / 100000);
- return Letters.Substring(bigSquareX + bigSquareY * 5, 1) + Letters.Substring(smallSquareX + smallSquareY * 5, 1);
- }
- public static string GetGridRef(this IGridReference self, int precision = 5)
- {
- int remainderX = (int)Math.Floor((self.X + 1000000) % 100000);
- int remainderY = (int)Math.Floor((self.Y + 1000000) % 100000);
- return GetGridRefLetters(self) + remainderX.ToString("00000").Substring(0, precision) + remainderY.ToString("00000").Substring(0, precision);
- }
- private class GridReferenceSimple : IGridReference
- {
- public double X { get; internal set; }
- public double Y { get; internal set; }
- }
- public static IGridReference ParseGridRef(this string source)
- {
- int gridIndexBig = Letters.IndexOf(source[0]);
- int gridIndexSmall = Letters.IndexOf(source[1]);
- int codeLength = (source.Length - 2) / 2;
- int gridX = int.Parse(source.Substring(2, codeLength));
- int gridY = int.Parse(source.Substring(2 + codeLength, codeLength));
- while (codeLength++ < 5)
- {
- gridX *= 10;
- gridY *= 10;
- }
- return new GridReferenceSimple
- {
- X = gridX + 100000 * (gridIndexSmall % 5 + 5 * (gridIndexBig % 5 - 2)),
- Y = gridY + 100000 * (4 - gridIndexSmall / 5 + 5 * (3 - gridIndexBig / 5)),
- };
- }
- static HashSet<string> ValidAreas = new HashSet<string>
- {
- "HP",
- "HT", "HU",
- "HW", "HX", "HY", "HZ",
- "NA", "NB", "NC", "ND",
- "NF", "NG", "NH", "NJ", "NK",
- "NL", "NM", "NN", "NO",
- "NR", "NS", "NT", "NU",
- "NW", "NX", "NY", "NZ", "OV",
- "SC", "SD", "SE", "TA",
- "SH", "SJ", "SK", "TF", "TG",
- "SM", "SN", "SO", "SP", "TL", "TM",
- "SR", "SS", "ST", "SU", "TQ", "TR",
- "SV", "SW", "SX", "SY", "SZ", "TV",
- };
- public static bool IsWithinStandardArea(this IGridReference self)
- {
- if (self.X >= 0 && self.X < 700000 && self.Y >= 0 && self.Y < 1300000)
- return ValidAreas.Contains(self.GetGridRefLetters());
- return false;
- }
- }
- public class StationCode2 : IStationCode
- {
- public StationCode2(StationCode a, StationCode b)
- {
- A = a;
- B = b;
- }
- StationCode A { get; }
- StationCode B { get; }
- public string Code => A.Code + B.Code;
- public string Name => $"Half way from {A.Name} to {B.Name}";
- public string FullInfo => $"{ToString()}\n{A}\n{B}";
- public double X => (A.X + B.X) / 2;
- public double Y => (A.Y + B.Y) / 2;
- public override string ToString()
- {
- return $"{this.GetGridRef()}({X},{Y}): {Code} {Name}";
- }
- }
- public class StationCode3 : IStationCode
- {
- public StationCode3(StationCode a, StationCode b, StationCode c) : this(a, b, c, a.GetDist2(b), a.GetDist2(c)) { }
- public StationCode3(StationCode a, StationCode b, StationCode c, double distAB2, double distAC2)
- {
- A = a;
- B = b;
- C = c;
- // arbitrarily treat A as the origin... so aX and aY are implicitly zero in intermediate calculations.
- double bX = b.X - a.X;
- double bY = b.Y - a.Y;
- double cX = c.X - a.X;
- double cY = c.Y - a.Y;
- double factor = 2 * (bX * cY - cX * bY);
- X = a.X + (distAB2 * cY - distAC2 * bY) / factor;
- Y = a.Y + (distAC2 * bX - distAB2 * cX) / factor;
- /* Console.WriteLine(A);
- Console.WriteLine(B);
- Console.WriteLine(C);
- Console.WriteLine($"{distAB2},{distBC2},{distAC2}");
- Console.WriteLine($"{X},{Y} => {A.GetDist2(this)},{B.GetDist2(this)},{C.GetDist2(this)}");*/
- }
- StationCode A { get; }
- StationCode B { get; }
- StationCode C { get; }
- public string Code => A.Code + B.Code + C.Code;
- public string Name => $"Circle: {A.Name}, {B.Name}, {C.Name}";
- public string FullInfo => $"{this}\n{A}\n{B}\n{C}";
- public double X { get; }
- public double Y { get; }
- public override string ToString()
- {
- return $"{this.GetGridRef()}({X:0.0},{Y:0.0}): {Code} {Name}";
- }
- }
- public class StationCodeTable
- {
- public static void Main()
- {
- var stationCodeTable = new StationCodeTable("RailReferences.csv");
- for (int i = 0; i < 10; i++)
- stationCodeTable.MonteCarlo(i, 20000);
- while(true)
- {
- Console.Write("Code or co-ordinates:");
- var input = Console.ReadLine();
- var splitStrings = input.Split(',');
- var in0 = splitStrings[0];
- var inputUpper = input.ToUpperInvariant();
- if (inputUpper.All(c => c >= 'A' && c <= 'Z'))
- {
- var value = stationCodeTable.Decode(inputUpper, true);
- if (!(value is InvalidCodeSentinel) && !value.IsWithinStandardArea())
- Console.WriteLine(InvalidCodeReason.OutsideArea);
- Console.WriteLine(value.FullInfo);
- }
- else if (in0.EndsWith(".txt") || splitStrings.All(s => s.All(c => c >= 'A' && c <= 'Z' || c == '?')))
- {
- Dictionary<InvalidCodeReason, List<string>> InvalidCodes = new Dictionary<InvalidCodeReason, List<string>>();
- Dictionary<string, HashSet<string>> namesForGridRef = new Dictionary<string, HashSet<string>>();
- List<string> urlFragments = new List<string>();
- foreach (var pair in in0.EndsWith(".txt") ? stationCodeTable.GetCodesFromFile(in0, (splitStrings.Length >=2) ? splitStrings[1] : null)
- : stationCodeTable.GetCodes(splitStrings))
- {
- // now only done for VALID codes
- // Console.WriteLine($"{pair.Key} : {pair.Value}");
- InvalidCodeReason? errorCode = null;
- if (pair.Value is InvalidCodeSentinel sentinel)
- errorCode = sentinel.Reason;
- else if (!pair.Value.IsWithinStandardArea())
- errorCode = InvalidCodeReason.OutsideArea;
- else
- {
- Console.WriteLine($"{pair.Key} : {pair.Value}");
- string gridRef = pair.Value.GetGridRef();
- if (!namesForGridRef.TryGetValue(gridRef, out var list))
- namesForGridRef.Add(gridRef, list = new HashSet<string>());
- list.Add(pair.Key);
- }
- if (errorCode.HasValue)
- {
- if (!InvalidCodes.TryGetValue(errorCode.Value, out var list))
- InvalidCodes.Add(errorCode.Value, list = new List<string>());
- list.Add(pair.Key);
- }
- }
- foreach (var pair in InvalidCodes)
- Console.WriteLine($"{pair.Key}: {string.Join(" ", pair.Value)}");
- Console.WriteLine($"Drawing map for: " + string.Join(" ", namesForGridRef.Values.Select(set => string.Join("/", set))));
- foreach (var pair in namesForGridRef)
- urlFragments.Add($"{pair.Key}|{string.Join("/", pair.Value)}|1");
- Console.WriteLine($"https://gridreferencefinder.com/?gr={string.Join(",", urlFragments)}&v=r&labels=1");
- Console.WriteLine($"https://gridreferencefinder.com/osm/?gr={string.Join(",", urlFragments)}&v=r&labels=1");
- }
- else if (splitStrings.Length >= 2 && int.TryParse(splitStrings[0], out int x) && int.TryParse(splitStrings[1], out int y))
- {
- if (splitStrings.Length < 3 || !int.TryParse(splitStrings[2], out int resolution))
- resolution = 50;
- if (splitStrings.Length < 4 || !int.TryParse(splitStrings[3], out int option))
- option = 1;
- Console.WriteLine(stationCodeTable.Encode(x, y, resolution, option));
- }
- else if (in0.Length > 5 && in0.Length % 2 == 0 && char.IsLetter(in0[0]) && char.IsLetter(in0[1]) && in0.Substring(2).All(char.IsNumber) &&
- in0.ParseGridRef() is var gridRef)
- {
- if (splitStrings.Length < 2 || !int.TryParse(splitStrings[1], out int resolution))
- resolution = 50;
- if (splitStrings.Length < 3 || !int.TryParse(splitStrings[2], out int option))
- option = 1;
- Console.WriteLine(stationCodeTable.Encode((int)gridRef.X, (int)gridRef.Y, resolution, option));
- }
- }
- }
- public static IEnumerable<string> ReadFileLines(string inputFile)
- {
- using (var file = new StreamReader(inputFile))
- {
- while (!file.EndOfStream)
- yield return file.ReadLine();
- }
- }
- public StationCodeTable(string inputFile)
- {
- SingleCodes = new Dictionary<string, StationCode>(2632);
- AmbiguousCodes = new HashSet<string>() { "SHT", "YYZ" };
- ShortCodes = new Dictionary<string, StationCode>(26 * 26);
- foreach(string line in ReadFileLines(inputFile))
- {
- if (line.StartsWith("\"AtcoCode\",\"TiplocCode\",\"CrsCode\""))
- StationCode.InitFields(line);
- else if (line.Length > 4)
- {
- var newCode = new StationCode(line);
- if (!AmbiguousCodes.Contains(newCode.Code))
- {
- string shortCode = newCode.Code.Substring(0, 2);
- if (SingleCodes.TryGetValue(newCode.Code, out var oldCode))
- {
- if (oldCode.X != newCode.X || oldCode.Y != newCode.Y)
- {
- AmbiguousCodes.Add(newCode.Code);
- SingleCodes.Remove(newCode.Code);
- ShortCodes[shortCode] = null;
- }
- else
- Console.WriteLine($"Exact duplicates:\n[{oldCode}] and\n[{newCode}]");
- }
- else
- {
- SingleCodes.Add(newCode.Code, newCode);
- ShortCodes[shortCode] = ShortCodes.TryGetValue(shortCode, out _) ? null : newCode;
- }
- }
- }
- }
- Console.WriteLine($"\n{SingleCodes.Count} lines read");
- Console.WriteLine("Short codes:");
- foreach(var pair in ShortCodes.OrderBy(pair => pair.Key).Where(pair => pair.Value!=null))
- Console.WriteLine($"{pair.Key}: {pair.Value}");
- Console.WriteLine("Ambiguous 3-codes: "+string.Join(",", AmbiguousCodes));
- var codes = SingleCodes.Keys.ToList();
- double minDist2 = double.MaxValue;
- for (int i = 0; i < SingleCodes.Count; i++)
- {
- StationCode a = SingleCodes[codes[i]];
- for (int j = i; j < SingleCodes.Count; j++)
- {
- StationCode b = SingleCodes[codes[j]];
- double distAB2 = a.GetDist2(b);
- if (j == i)
- ProcessCode(a);
- else if (distAB2 > 10000) // don't generate a code2 if the originals were within 100m.
- {
- var code = new StationCode2(a, b);
- ProcessCode(code);
- minDist2 = Math.Min(minDist2, distAB2);
- }
- }
- }
- Console.WriteLine($"Done first pass. {CodesProcessed} codes processed, {Duplicates} duplicates");
- }
- Dictionary<string, StationCode> SingleCodes { get; }
- Dictionary<string, StationCode> ShortCodes { get; }
- Dictionary<string, IStationCode> LoadedFileCodes { get; set; }
- HashSet<string> AmbiguousCodes { get; }
- int CodesProcessed = 0;
- int Duplicates = 0;
- const int GridResolution = 100;
- // AllCodes contains only 1 and 2 part codes.
- // 3 part codes are too numerous, so are generated dynamically.
- Dictionary<int, Dictionary<int, List<IStationCode>>> AllCodes { get; } = new Dictionary<int, Dictionary<int, List<IStationCode>>>(10000);
- void ProcessCode(IStationCode code)
- {
- int boxX = (int)Math.Floor(code.X / GridResolution);
- int boxY = (int)Math.Floor(code.Y / GridResolution);
- if (!AllCodes.TryGetValue(boxY, out var rowDict))
- AllCodes.Add(boxY, rowDict = new Dictionary<int, List<IStationCode>>(5000));
- if (rowDict.TryGetValue(boxX, out var codeList))
- Duplicates++;
- else
- rowDict.Add(boxX, codeList = new List<IStationCode>());
- codeList.Add(code);
- CodesProcessed++;
- }
- public void Export(string fileName)
- {
- using (var file = new StreamWriter(fileName))
- {
- foreach (var rowPair in AllCodes)
- {
- int y = rowPair.Key;
- foreach(var pair in rowPair.Value)
- {
- file.WriteLine($"{pair.Key},{y},{string.Join("/",pair.Value.Select(c => c.Code))}");
- }
- }
- }
- Console.WriteLine("Exported to "+fileName);
- }
- Dictionary<string, IStationCode> GetCodesFromFile(string inputFile, string conditionText = null)
- {
- return GetCodes(ReadFileLines(inputFile), conditionText);
- }
- Dictionary<string, IStationCode> GetCodes(IEnumerable<string> inputLines, string conditionText = null)
- {
- Func<string, bool> condition = s => true;
- if (conditionText != null)
- {
- if (int.TryParse(conditionText, out int codeLength))
- condition = s => s.Length == codeLength;
- else if (conditionText == "*")
- condition = s => s.Length == 6 &&
- ShortCodes.TryGetValue(s.Substring(0, 2), out var short1) && short1 != null &&
- ShortCodes.TryGetValue(s.Substring(2, 2), out var short2) && short2 != null &&
- ShortCodes.TryGetValue(s.Substring(4, 2), out var short3) && short3 != null;
- else if (conditionText.All(char.IsLetter) && conditionText.ToUpperInvariant() is string upperConditionText)
- condition = s => s.Contains(upperConditionText);
- }
- var result = new Dictionary<string, IStationCode>();
- var iso8859_8 = Encoding.GetEncoding("ISO-8859-8");
- foreach (string line in inputLines)
- {
- if (line.Length > 20)
- Console.WriteLine("Long line ignored: " + line);
- else
- {
- var bytes = iso8859_8.GetBytes(line);
- string normalisedLine = Encoding.UTF8.GetString(bytes).ToUpperInvariant();
- Stack<string> linesToTry = new Stack<string>();
- linesToTry.Push(normalisedLine);
- while (linesToTry.Count > 0)
- {
- string code = "";
- normalisedLine = linesToTry.Pop();
- for (int i = 0; i < normalisedLine.Length; i++)
- {
- char c = normalisedLine[i];
- if (c >= 'A' && c <= 'Z')
- code += c;
- else if(c == '?')
- {
- for (c = 'Z'; c > 'A'; --c)
- linesToTry.Push(code + c + normalisedLine.Substring(i + 1));
- code += 'A';
- }
- }
- if (condition(code) && !result.ContainsKey(code))
- result.Add(code, Decode(code));
- }
- }
- }
- LoadedFileCodes = result.Where(pair => !(pair.Value is InvalidCodeSentinel) && pair.Value.IsWithinStandardArea()).ToDictionary(pair => pair.Key, pair => pair.Value);
- return result;
- }
- string Multi3CodeMessage(string ambiguous2code)
- {
- return "Multiple 3-codes: " + string.Join(" ", SingleCodes.Keys.Concat(AmbiguousCodes).Where(s => s.StartsWith(ambiguous2code)));
- }
- string Disambiguate(string original, Action<string> ambiguousAction = null)
- {
- // if ambiguous we return string.Empty.
- // if completely invalid, return the original string so that Disambiguate can return "Not Found".
- // Otherwise, all codes are expanded to the canonical 3-character form.
- bool actionDone = false;
- Action<string> recursiveAction = null;
- if (ambiguousAction != null)
- recursiveAction = s => { actionDone = true; ambiguousAction(s); };
- switch (original.Length)
- {
- case 2: // Many 2-codes are ambiguous...
- if (ShortCodes.TryGetValue(original, out var info))
- {
- if (info?.Code == null)
- ambiguousAction?.Invoke(Multi3CodeMessage(original));
- return info?.Code ?? string.Empty;
- }
- goto default; // will be "not found".
- case 3:
- if (AmbiguousCodes.Contains(original))
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original}]");
- return string.Empty;
- }
- goto default; // either found as normal, or not found.
- case 4: // must be split as two 2-codes
- {
- if (ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) &&
- ShortCodes.TryGetValue(original.Substring(2, 2), out var info2))
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : 2, 2)));
- return (info1 == null || info2 == null) ? string.Empty : info1.Code + info2.Code;
- }
- }
- goto default; // will be "not found".
- case 5: // can be 2-code followed by 3-code or 3-code followed by 2-code.
- {
- string disambiguatedLongEnd = Disambiguate(original.Substring(2, 3), recursiveAction);
- bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) && (disambiguatedLongEnd == string.Empty || SingleCodes.ContainsKey(disambiguatedLongEnd));
- string disambiguatedLongStart = Disambiguate(original.Substring(0, 3), recursiveAction);
- bool shortEnd = ShortCodes.TryGetValue(original.Substring(3, 2), out var info2) && (disambiguatedLongStart == string.Empty || SingleCodes.ContainsKey(disambiguatedLongStart));
- if (shortStart && shortEnd)
- {
- ambiguousAction?.Invoke("Can be split as 2+3 or 3+2"); // most relevant - overrides ambiguous 3-code from recursiveAction
- return string.Empty;
- }
- else if (shortStart && (info1 == null || disambiguatedLongEnd == string.Empty) || shortEnd && (info2 == null || disambiguatedLongStart == string.Empty))
- {
- if (!actionDone)
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : 3, 2)));
- return string.Empty;
- }
- if (shortStart)
- return info1.Code + disambiguatedLongEnd;
- if (shortEnd)
- return disambiguatedLongStart + info2.Code;
- }
- goto default;
- case 6: // can be 2 3-codes or 3 2-codes
- {
- bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
- bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(3, 3));
- // Without the following early exit, too many 6-letter codes come up "ambiguous".
- // It considers 2+2+2 codes ONLY if the 3+3 code is not valid.
- // (this is reason for "hooks into the NotFound error handler" clue in published puzzle
- // other codes are only explored if the original algorithm that deals only with 6 codes fails).
- if (unambiguous1 && unambiguous2)
- goto default;
- bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
- bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(3, 3));
- if (ambiguous1 && (ambiguous2 || unambiguous2) || unambiguous1 && ambiguous2)
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 3, 3)}]");
- return string.Empty; // ambiguous before we even consider short codes.
- }
- // else either there's no valid 3+3 code, or it would, in itself, be unambiguous.
- // check for a 2+2+2 code.
- if (ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) &&
- ShortCodes.TryGetValue(original.Substring(2, 2), out var info2) &&
- ShortCodes.TryGetValue(original.Substring(4, 2), out var info3))
- {
- // the following if() is now redundant, as we always favour a valid
- // 6 letter code when it exists.
- if (unambiguous1 && unambiguous2)
- {
- ambiguousAction?.Invoke("Can be split as 3+3 or 2+2+2"); // most relevant - overrides ambiguous 2-codes
- return string.Empty;
- }
- if (info1 == null || info2 == null || info3 == null)
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : info2 == null ? 2 : 4, 2)));
- return string.Empty;
- }
- return info1.Code + info2.Code + info3.Code;
- }
- }
- goto default;
- case 7: // must be 1 3-code and 2 2-codes. 3-code can be in one of 3 positions.
- {
- bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
- bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(2, 3));
- bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(4, 3));
- // in this case, shortStart etc. say nothing of whether they form part of a valid code.
- bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var infoStart);
- bool shortMid1 = ShortCodes.TryGetValue(original.Substring(2, 2), out var infoMid1);
- bool shortMid2 = ShortCodes.TryGetValue(original.Substring(3, 2), out var infoMid2);
- bool shortEnd = ShortCodes.TryGetValue(original.Substring(5, 2), out var infoEnd);
- if (ambiguous1 && shortMid2 && shortEnd)
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(0, 3)}]");
- return string.Empty; // an ambiguous 3-code is valid in position 1.
- }
- if (shortStart && ambiguous2 && shortEnd)
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(2, 3)}]");
- return string.Empty; // an ambiguous 3-code is valid in position 2.
- }
- if (shortStart && shortMid1 && ambiguous3)
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(4, 3)}]");
- return string.Empty; // an ambiguous 3-code is valid in position 3.
- }
- bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
- bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(2, 3));
- bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(4, 3));
- string expanded = null;
- if (unambiguous1 && shortMid2 && shortEnd)
- {
- if (infoMid2 == null || infoEnd == null)
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoMid2 == null ? 3 : 5, 2)));
- return string.Empty;
- }
- expanded = original.Substring(0, 3) + infoMid2.Code + infoEnd.Code;
- }
- if (shortStart && unambiguous2 && shortEnd)
- {
- if (infoStart == null || infoEnd == null)
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoStart == null ? 0 : 5, 2)));
- return string.Empty;
- }
- if(expanded!=null)
- {
- ambiguousAction?.Invoke($"Can be interpreted as {expanded} or {infoStart.Code + original.Substring(2, 3) + infoEnd.Code}");
- return string.Empty;
- }
- expanded = infoStart.Code + original.Substring(2, 3) + infoEnd.Code;
- }
- if (shortStart && shortMid1 && unambiguous3)
- {
- if (infoStart == null || infoMid1 == null)
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoMid2 == null ? 0 : 2, 2)));
- return string.Empty;
- }
- if (expanded != null)
- {
- ambiguousAction?.Invoke($"Can be interpreted as {expanded} or {infoStart.Code + infoMid1.Code + original.Substring(4, 3)}");
- return string.Empty;
- }
- expanded = infoStart.Code + infoMid1.Code + original.Substring(4, 3);
- }
- return expanded ?? original;
- }
- case 8: // must be 2 3-codes and a 1-code. 2-code can be in one of 3 positions.
- {
- bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
- bool ambiguous2a = AmbiguousCodes.Contains(original.Substring(2, 3));
- bool ambiguous2b = AmbiguousCodes.Contains(original.Substring(3, 3));
- bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(5, 3));
- bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
- bool unambiguous2a = SingleCodes.ContainsKey(original.Substring(2, 3));
- bool unambiguous2b = SingleCodes.ContainsKey(original.Substring(3, 3));
- bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(5, 3));
- // in this case, shortStart etc. are only set if the 3-codes are valid (albeit potentially ambiguous)
- bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var infoStart) && (ambiguous2a || unambiguous2a) && (ambiguous3 || unambiguous3);
- bool shortMid = ShortCodes.TryGetValue(original.Substring(3, 2), out var infoMid) && (ambiguous1 || unambiguous1) && (ambiguous3 || unambiguous3);
- bool shortEnd = ShortCodes.TryGetValue(original.Substring(6, 2), out var infoEnd) && (ambiguous1 || unambiguous1) && (ambiguous2b || unambiguous2b);
- if (shortStart && (shortMid || shortEnd) || shortMid && shortEnd)
- {
- ambiguousAction?.Invoke("Can be interpreted as" + (shortStart ? " 2+3+3" : "") + (shortMid ? " 3+2+3" : "") + (shortEnd ? " 3+3+2" : ""));
- return string.Empty;
- }
- // at most one of shortStart, shortMid or shortEnd is set now.
- if (!shortStart && !shortMid && !shortEnd)
- return original; // will cause "NotFound" error as length is not valid for caller.
- // exactly one of shortStart, shortMid or ShortEnd is set now.
- if (shortStart && (ambiguous2a || ambiguous3))
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous2a ? 2 : 5, 3)}]");
- return string.Empty;
- }
- else if(shortMid && (ambiguous1 || ambiguous3))
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 5, 3)}]");
- return string.Empty;
- }
- else if(shortEnd && (ambiguous1 || ambiguous2b))
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 3, 3)}]");
- return string.Empty;
- }
- if ((shortStart ? infoStart : shortMid ? infoMid : infoEnd) == null)
- {
- ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(shortStart ? 0 : shortMid ? 3 : 6, 2)));
- return string.Empty;
- }
- return shortStart ? infoStart.Code + original.Substring(2, 6) :
- shortMid ? original.Substring(0, 3) + infoMid.Code + original.Substring(5, 3) :
- shortEnd ? original.Substring(0, 6) + infoEnd.Code :
- throw new InvalidOperationException("At least one must be set at this point in code!");
- }
- case 9: // if it's not ambiguous, it's already in canonical form.
- {
- bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
- bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(3, 3));
- bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(6, 3));
- bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
- bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(3, 3));
- bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(6, 3));
- if (ambiguous1 && (ambiguous2 || unambiguous2) && (ambiguous3 || unambiguous3) ||
- unambiguous1 && (ambiguous2 && (ambiguous3 || unambiguous3) || unambiguous2 && ambiguous3))
- {
- ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : ambiguous2 ? 6 : 9, 3)}]");
- return string.Empty;
- }
- }
- goto default;
- default: return original;
- }
- }
- IStationCode Decode(string codeString, bool explainAmbiguities = false)
- {
- string ambiguousReason = null;
- codeString = explainAmbiguities ? Disambiguate(codeString, s => ambiguousReason = s) : Disambiguate(codeString);
- if (codeString.Length > 9)
- return (InvalidCodeSentinel)InvalidCodeReason.TooLong;
- switch (codeString.Length)
- {
- case 0:
- if (!string.IsNullOrEmpty(ambiguousReason))
- return new InvalidCodeSentinel(InvalidCodeReason.Ambiguous, ambiguousReason);
- return (InvalidCodeSentinel)InvalidCodeReason.Ambiguous;
- case 3:
- if (SingleCodes.TryGetValue(codeString, out var stationCode))
- return stationCode;
- break;
- case 6:
- if (SingleCodes.TryGetValue(codeString.Substring(0, 3), out var codeA))
- if (SingleCodes.TryGetValue(codeString.Substring(3, 3), out var codeB))
- return new StationCode2(codeA, codeB);
- break;
- case 9:
- if (SingleCodes.TryGetValue(codeString.Substring(0, 3), out var a))
- if (SingleCodes.TryGetValue(codeString.Substring(3, 3), out var b))
- if (SingleCodes.TryGetValue(codeString.Substring(6, 3), out var c))
- {
- double minDist2 = Math.Min(a.GetDist2(b), a.GetDist2(c));
- minDist2 = Math.Min(minDist2, b.GetDist2(c));
- var code = new StationCode3(a, b, c);
- if (a.GetDist2(code) > 100 * minDist2)
- Console.WriteLine("WARNING: Badly formed");
- return code;
- }
- break;
- }
- return (InvalidCodeSentinel)InvalidCodeReason.NotFound;
- }
- string Encode(int x, int y, int resolution = 50, int option = 1)
- {
- bool include3codes = option == 1 || option > 5;
- int minBoxX = (x - resolution) / GridResolution;
- int maxBoxX = (x + resolution) / GridResolution;
- int minBoxY = (y - resolution) / GridResolution;
- int maxBoxY = (y + resolution) / GridResolution;
- List<Tuple<IStationCode, double>> codes = new List<Tuple<IStationCode, double>>();
- // ignore distance parameter to find nearby codes from last processed file
- var fileCodesByDistance = LoadedFileCodes?.Values.OrderBy(code => code.GetDist2(x, y)).Take(3) ?? Enumerable.Empty<IStationCode>();
- foreach (var code in fileCodesByDistance)
- codes.Add(Tuple.Create(code, code.GetDist2(x, y)));
- int resolution2 = resolution * resolution;
- for (int boxY = minBoxY; boxY <= maxBoxY; boxY++)
- if (AllCodes.TryGetValue(boxY, out var rowDict))
- for (int boxX = minBoxX; boxX <= maxBoxX; boxX++)
- if (rowDict.TryGetValue(boxX, out var codeList))
- codes.AddRange(codeList.Select(code => Tuple.Create(code, code.GetDist2(x, y))).Where(t => t.Item2 <= resolution2));
- if (include3codes)
- {
- Console.WriteLine("Including 3-codes...");
- var targetsByDistance = SingleCodes.Values.OrderBy(sc => sc.GetDist2(x, y)).ToList();
- double prevDist = 0;
- double prev2Dist = 0;
- for (int i = 0; i < targetsByDistance.Count; i++)
- {
- double myDist = Math.Sqrt(targetsByDistance[i].GetDist2(x, y));
- double distAB2, distAC2, distBC2;
- if (i >= 2 && prev2Dist > myDist - resolution &&
- myDist * prev2Dist / 100 < (distAB2 = targetsByDistance[i - 2].GetDist2(targetsByDistance[i - 1])) &&
- myDist * prev2Dist / 100 < (distAC2 = targetsByDistance[i - 2].GetDist2(targetsByDistance[i])) &&
- myDist * prev2Dist / 100 < (distBC2 = targetsByDistance[i - 1].GetDist2(targetsByDistance[i])))
- {
- IStationCode code = new StationCode3(targetsByDistance[i - 2], targetsByDistance[i - 1], targetsByDistance[i], distAB2, distAC2);
- double dist2 = code.GetDist2(x, y);
- if (dist2 < resolution2)
- codes.Add(Tuple.Create(code, dist2));
- }
- prev2Dist = prevDist;
- prevDist = myDist;
- }
- }
- var filteredCodes = option < 2 ? codes :
- codes.Where(t => t.Item1.Code.Length == option || GetEquivalentCodes(t.Item1.Code).Any(s => s.Length == option));
- return string.Join("\n", filteredCodes.OrderBy(t=>t.Item2).Select(t=>$"{t.Item1.Code}({Math.Sqrt(t.Item2)}) : {t.Item1}\nAlternative forms:{string.Join("/",GetEquivalentCodes(t.Item1.Code))}"));
- }
- IEnumerable<string> GetEquivalentCodes(string codeString)
- {
- if (codeString.Length % 3 != 0 || codeString.Length == 0)
- throw new ArgumentOutOfRangeException(codeString);
- if (codeString.Length == 3)
- {
- string shortCode = codeString.Substring(0, 2);
- if (ShortCodes.TryGetValue(shortCode, out var codeInfo) && codeInfo != null)
- return new[] { codeString, shortCode };
- else if (SingleCodes.ContainsKey(codeString))
- return new[] { codeString };
- else
- return Enumerable.Empty<string>();
- }
- HashSet<string> LongCodes = new HashSet<string>();
- for (int i = 0; i < codeString.Length; i += 3)
- if (!LongCodes.Add(codeString.Substring(i, 3)))
- return Enumerable.Empty<string>();
- List<string> results = new List<string>();
- foreach(var longCode in LongCodes)
- {
- IEnumerable<string> longCodeForms = GetEquivalentCodes(longCode);
- IEnumerable<string> remainderForms = GetEquivalentCodes(string.Join("", LongCodes.Where(c => c != longCode)));
- foreach (var prefix in longCodeForms)
- foreach (var suffix in remainderForms)
- if (Disambiguate(prefix + suffix) != string.Empty)
- results.Add(prefix + suffix);
- }
- return results;
- }
- static IEnumerable<string> GenerateAllStrings(int length)
- {
- if (length == 0)
- yield return string.Empty;
- else
- foreach (var shorterString in GenerateAllStrings(length - 1))
- for (char addedChar = 'A'; addedChar <= 'Z'; addedChar++)
- yield return shorterString + addedChar;
- }
- static IEnumerable<string> GenerateRandomStrings(int length)
- {
- Random rand = new Random();
- char[] charArray = new char[length];
- while(true)
- {
- for (int i = 0; i < length; i++)
- charArray[i] = (char)rand.Next('A', 'Z');
- yield return new string(charArray);
- }
- }
- public void MonteCarlo(int length, int count)
- {
- double allValid = Math.Pow(26, length);
- IEnumerable<string> stringsToCheck;
- if (count > allValid * 0.99)
- {
- count = (int)Math.Round(allValid);
- stringsToCheck = GenerateAllStrings(length);
- }
- else
- stringsToCheck = GenerateRandomStrings(length).Distinct().Take(count);
- int totalAmbiguous = 0;
- int totalUnknown = 0;
- int totalOutsideArea = 0;
- int totalValid = 0;
- Console.WriteLine($"Running Monte-Carlo analysis for {count} strings of length {length}...");
- foreach(var testString in stringsToCheck)
- {
- var code = Decode(testString);
- switch ((code as InvalidCodeSentinel )?.Reason)
- {
- case InvalidCodeReason.NotFound: totalUnknown++; break;
- case InvalidCodeReason.Ambiguous: totalAmbiguous++; break;
- case null:
- if (code.IsWithinStandardArea())
- totalValid++;
- else
- totalOutsideArea++;
- break;
- default: throw new NotImplementedException();
- }
- }
- Console.WriteLine($"\nChecked length {length}. {totalValid*100.0/count:0}% valid, {totalUnknown * 100.0 / count:0}% invalid, {totalAmbiguous * 100.0 / count:0}% ambiguous, {totalOutsideArea * 100.0 / count:0} outside area");
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement