Advertisement
Guest User

StationCodes.cs

a guest
Aug 14th, 2020
204
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 42.46 KB | None | 0 0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6.  
  7. // The sole author of this program is https://puzzling.stackexchange.com/users/12582/steve
  8. // Supplied "as is". No support given, and no implied suitability for any purpose.
  9. // It was intended for my own use, and was heavily adapted during the time the puzzle was live
  10. // This is the version as of the final "(Conclusion)" update (with the following comment blocks added)
  11.  
  12.  
  13. // This is the program that was used behind the scenes for my puzzle at
  14. // https://puzzling.stackexchange.com/questions/100512/hacked-maps-back-in-the-uk
  15. // If you somehow found this program before the puzzle, you already know
  16. // the main answer just by seeing the filename and class names...
  17.  
  18. // It runs on the command line, so map output is via a URL to copy-paste into your browser.
  19. // Expects to find a copy of RailReferences.csv in the same folder for initialisation.
  20. // (if you don't know what that is or where to get it, this program probably isn't for you yet!)
  21.  
  22. // This program (together with appropriate third-party websites) takes the role of:
  23. // - the "hacked maps" system itself.
  24. // - the "companion program" that looks up keywords
  25. // - the program that "computer guy" made for me after the puzzle was solved.
  26.  
  27.  
  28. // Input of the following forms is recognised:
  29.  
  30. // CODE
  31. // - outputs full details of the specified code and any codes from which it was constructed.
  32. //   or full details of any "error code".
  33.  
  34. // filename.txt
  35. // - crashes with an exception if the file doesn't exist.
  36. //   otherwise, treats each line of the file as a code.
  37. //   Use this with dictionaries and other word lists.
  38. // - outputs details of any valid codes found, and then a classification by error code
  39. // - finally a URL that allows all of the valid codes to be viewed on a map.
  40.  
  41. // filename.txt,N
  42. // where N is a number from 2-9
  43. // - ignores any lines in the file not of the specified length.
  44. // - allows for quickly scanning a large dictionary and finding (e.g.) all valid 7-letter keywords.
  45. // - accented characters are converted to non-accented versions.
  46. // - non-alphabetic characters are ignored.
  47. // - '?' can be used as a wildcard character,
  48. //       and is expanded to a series of 26 strings replacing it with each of 'A' to 'Z'
  49.  
  50. // filename.txt,CODE
  51. // filters the file to include only lines containing the specified CODE as a substring
  52.  
  53. // filename.txt,*
  54. // searches specifically for 6-letter keywords consisting of 3 two-letter codes.
  55.  
  56.  
  57. // CODEA,CODEB,CODEC
  58. // same handling as filename.txt
  59. // also used for a single wild-carded code.
  60.  
  61. // Eastings,Northings
  62. // finds valid keywords within a default radius of 50m
  63. // (for 3-codes, only valid keywords of 3 consecutive distances are considered)
  64. // In addition, the closest 3 valid keywords from the most recently parsed dictionary are shown.
  65. // A known bug that I can't be bothered to fix right now is that some keywords outside a 25m radius
  66. // may be missed, as they have more than a 50m difference between stations in opposite directions.
  67.  
  68. // Eastings,Northings,Radius
  69. // Eastings,Northings,Radius,Option
  70. // finds valid keywords within a specified radius
  71. // Option 1 (default) finds all codes
  72. // Option 0 ignores 3 part codes, searching only for 2 part codes and dictionary entries.
  73. // Option 2-9 : finds only keywords of the specified length.
  74.  
  75. // GridRef
  76. // GridRef,Radius
  77. // GridRef,Radius,Option
  78. // same as for Eastings,Northings, but specified via an OS grid reference (with no spaces)
  79.  
  80.  
  81. public interface IGridReference
  82. {
  83.   double X { get; }
  84.   double Y { get; }
  85. }
  86.  
  87. public interface IStationCode : IGridReference
  88. {
  89.   string Code { get; }
  90.   string Name { get; }
  91.   string FullInfo { get; }
  92. }
  93.  
  94. public enum InvalidCodeReason
  95. {
  96.   Ambiguous,
  97.   NotFound,
  98.   OutsideArea,
  99.   TooLong,
  100. }
  101. public class InvalidCodeSentinel : IStationCode
  102. {
  103.   public InvalidCodeSentinel(InvalidCodeReason reason, string message)
  104.   {
  105.     Reason = reason;
  106.     Message = message;
  107.   }
  108.   public InvalidCodeReason Reason { get; }
  109.   public string Message { get; }
  110.  
  111.   public double X => double.NaN;
  112.   public double Y => double.NaN;
  113.  
  114.   public string Code => Reason.ToString();
  115.   public string Name => string.IsNullOrEmpty(Message) ? Code : Message;
  116.   public string FullInfo => string.IsNullOrEmpty(Message) ? Code : ToString();
  117.  
  118.   public override string ToString() => $"{Code}: {Name}";
  119.  
  120.   private static Dictionary<InvalidCodeReason, InvalidCodeSentinel> DefaultSentinelsDict = new Dictionary<InvalidCodeReason, InvalidCodeSentinel>();
  121.  
  122.   public static implicit operator InvalidCodeSentinel(InvalidCodeReason reason)
  123.   {
  124.     if (!DefaultSentinelsDict.TryGetValue(reason, out var result))
  125.       DefaultSentinelsDict.Add(reason, result = new InvalidCodeSentinel(reason, null));
  126.     return result;
  127.   }
  128. }
  129.  
  130. public class StationCode : IStationCode
  131. {
  132.   public static void ParseLine(string line, Action<int, string> fieldAction)
  133.   {
  134.     int lastIndex = 0;
  135.     int field = 0;
  136.     while (line.IndexOf(',', lastIndex + 1) is int nextIndex && nextIndex > -1)
  137.     {
  138.       string text = line.Substring(lastIndex + 1, nextIndex - lastIndex - 1);
  139.       fieldAction(field, text);
  140.       field++;
  141.       lastIndex = nextIndex;
  142.     }
  143.   }
  144.  
  145.   enum FieldNames
  146.   {
  147.     Unknown,
  148.     CrsCode,
  149.     StationName,
  150.     Easting,
  151.     Northing,
  152.   }
  153.  
  154.   static FieldNames[] Fields = new FieldNames[16];
  155.  
  156.   public static void InitFields(string fieldNamesLine)
  157.   {
  158.     ParseLine(fieldNamesLine, (i, text) => Fields[i] = Enum.TryParse<FieldNames>(text.Trim('\"'), out var name) ? name : FieldNames.Unknown);
  159.   }
  160.  
  161.   public StationCode(string line)
  162.   {
  163.     string[] TextsToTrim = new[] { " Rail Station", " (Rail Station)", " Railway Station" };
  164.     void initField(int fieldIndex, string text)
  165.     {
  166.       switch (Fields[fieldIndex])
  167.       {
  168.         case FieldNames.CrsCode: Code = text.Trim('\"'); break;
  169.         case FieldNames.StationName:
  170.           Name = text.Trim('\"');
  171.           string textToTrim = TextsToTrim.FirstOrDefault(Name.EndsWith);
  172.           if (textToTrim == null)
  173.             Console.WriteLine("What should I trim from: " + text);
  174.           else
  175.             Name = Name.Substring(0, Name.Length - textToTrim.Length);
  176.           break;
  177.         case FieldNames.Easting: X = Convert.ToDouble(text); break;
  178.         case FieldNames.Northing: Y = Convert.ToDouble(text); break;
  179.       }
  180.     }
  181.     ParseLine(line, initField);
  182.   }
  183.  
  184.   public string Code { get; private set; }
  185.   public string Name { get; private set; }
  186.   public string FullInfo => ToString();
  187.   public double X { get; private set; }
  188.   public double Y { get; private set; }
  189.  
  190.   public override string ToString()
  191.   {
  192.     return $"{this.GetGridRef()}({X},{Y}): {Code} {Name}";
  193.   }
  194. }
  195.  
  196. public static class StationCodeExtension
  197. {
  198.   public static double GetDist2(this IStationCode self, IStationCode other)
  199.   {
  200.     return (self.X - other.X) * (self.X - other.X) + (self.Y - other.Y) * (self.Y - other.Y);
  201.   }
  202.   public static double GetDist2(this IStationCode self, double otherX, double otherY)
  203.   {
  204.     return (self.X - otherX) * (self.X - otherX) + (self.Y - otherY) * (self.Y - otherY);
  205.   }
  206.  
  207.   private const string Letters = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
  208.   public static string GetGridRefLetters(this IGridReference self)
  209.   {
  210.     int bigSquareX = 2 + (int)Math.Floor(self.X / 500000);
  211.     int bigSquareY = 3 - (int)Math.Floor(self.Y / 500000);
  212.     if (bigSquareX < 0 || bigSquareX >= 5 || bigSquareY < 0 || bigSquareY >= 5)
  213.       return "##";
  214.     int smallSquareX = (int)Math.Floor(((self.X + 1000000) % 500000) / 100000);
  215.     int smallSquareY = 4 - (int)Math.Floor(((self.Y + 1000000) % 500000) / 100000);
  216.     return Letters.Substring(bigSquareX + bigSquareY * 5, 1) + Letters.Substring(smallSquareX + smallSquareY * 5, 1);
  217.   }
  218.   public static string GetGridRef(this IGridReference self, int precision = 5)
  219.   {
  220.     int remainderX = (int)Math.Floor((self.X + 1000000) % 100000);
  221.     int remainderY = (int)Math.Floor((self.Y + 1000000) % 100000);
  222.     return GetGridRefLetters(self) + remainderX.ToString("00000").Substring(0, precision) + remainderY.ToString("00000").Substring(0, precision);
  223.   }
  224.  
  225.   private class GridReferenceSimple : IGridReference
  226.   {
  227.     public double X { get; internal set; }
  228.     public double Y { get; internal set; }
  229.   }
  230.  
  231.   public static IGridReference ParseGridRef(this string source)
  232.   {
  233.     int gridIndexBig = Letters.IndexOf(source[0]);
  234.     int gridIndexSmall = Letters.IndexOf(source[1]);
  235.     int codeLength = (source.Length - 2) / 2;
  236.     int gridX = int.Parse(source.Substring(2, codeLength));
  237.     int gridY = int.Parse(source.Substring(2 + codeLength, codeLength));
  238.     while (codeLength++ < 5)
  239.     {
  240.       gridX *= 10;
  241.       gridY *= 10;
  242.     }
  243.     return new GridReferenceSimple
  244.     {
  245.       X = gridX + 100000 * (gridIndexSmall % 5 + 5 * (gridIndexBig % 5 - 2)),
  246.       Y = gridY + 100000 * (4 - gridIndexSmall / 5 + 5 * (3 - gridIndexBig / 5)),
  247.     };
  248.  
  249.   }
  250.  
  251.   static HashSet<string> ValidAreas = new HashSet<string>
  252.   {
  253.                             "HP",
  254.                       "HT", "HU",
  255.           "HW", "HX", "HY", "HZ",
  256.     "NA", "NB", "NC", "ND",
  257.     "NF", "NG", "NH", "NJ", "NK",
  258.     "NL", "NM", "NN", "NO",
  259.           "NR", "NS", "NT", "NU",
  260.           "NW", "NX", "NY", "NZ", "OV",
  261.                 "SC", "SD", "SE", "TA",
  262.                 "SH", "SJ", "SK", "TF", "TG",
  263.           "SM", "SN", "SO", "SP", "TL", "TM",
  264.           "SR", "SS", "ST", "SU", "TQ", "TR",
  265.     "SV", "SW", "SX", "SY", "SZ", "TV",
  266.   };
  267.  
  268.   public static bool IsWithinStandardArea(this IGridReference self)
  269.   {
  270.     if (self.X >= 0 && self.X < 700000 && self.Y >= 0 && self.Y < 1300000)
  271.       return ValidAreas.Contains(self.GetGridRefLetters());
  272.     return false;
  273.   }
  274. }
  275.  
  276. public class StationCode2 : IStationCode
  277. {
  278.   public StationCode2(StationCode a, StationCode b)
  279.   {
  280.     A = a;
  281.     B = b;
  282.   }
  283.  
  284.   StationCode A { get; }
  285.   StationCode B { get; }
  286.  
  287.   public string Code => A.Code + B.Code;
  288.   public string Name => $"Half way from {A.Name} to {B.Name}";
  289.   public string FullInfo => $"{ToString()}\n{A}\n{B}";
  290.   public double X => (A.X + B.X) / 2;
  291.   public double Y => (A.Y + B.Y) / 2;
  292.  
  293.   public override string ToString()
  294.   {
  295.     return $"{this.GetGridRef()}({X},{Y}): {Code} {Name}";
  296.   }
  297. }
  298.  
  299. public class StationCode3 : IStationCode
  300. {
  301.   public StationCode3(StationCode a, StationCode b, StationCode c) : this(a, b, c, a.GetDist2(b), a.GetDist2(c)) { }
  302.  
  303.   public StationCode3(StationCode a, StationCode b, StationCode c, double distAB2, double distAC2)
  304.   {
  305.     A = a;
  306.     B = b;
  307.     C = c;
  308.     // arbitrarily treat A as the origin... so aX and aY are implicitly zero in intermediate calculations.
  309.     double bX = b.X - a.X;
  310.     double bY = b.Y - a.Y;
  311.     double cX = c.X - a.X;
  312.     double cY = c.Y - a.Y;
  313.     double factor = 2 * (bX * cY - cX * bY);
  314.     X = a.X + (distAB2 * cY - distAC2 * bY) / factor;
  315.     Y = a.Y + (distAC2 * bX - distAB2 * cX) / factor;
  316. /*    Console.WriteLine(A);
  317.     Console.WriteLine(B);
  318.     Console.WriteLine(C);
  319.     Console.WriteLine($"{distAB2},{distBC2},{distAC2}");
  320.     Console.WriteLine($"{X},{Y} => {A.GetDist2(this)},{B.GetDist2(this)},{C.GetDist2(this)}");*/
  321.   }
  322.  
  323.   StationCode A { get; }
  324.   StationCode B { get; }
  325.   StationCode C { get; }
  326.  
  327.   public string Code => A.Code + B.Code + C.Code;
  328.   public string Name => $"Circle: {A.Name}, {B.Name}, {C.Name}";
  329.   public string FullInfo => $"{this}\n{A}\n{B}\n{C}";
  330.   public double X { get; }
  331.   public double Y { get; }
  332.  
  333.   public override string ToString()
  334.   {
  335.     return $"{this.GetGridRef()}({X:0.0},{Y:0.0}): {Code} {Name}";
  336.   }
  337. }
  338.  
  339.  
  340. public class StationCodeTable
  341. {
  342.   public static void Main()
  343.   {
  344.     var stationCodeTable = new StationCodeTable("RailReferences.csv");
  345.  
  346.     for (int i = 0; i < 10; i++)
  347.       stationCodeTable.MonteCarlo(i, 20000);
  348.  
  349.     while(true)
  350.     {
  351.       Console.Write("Code or co-ordinates:");
  352.       var input = Console.ReadLine();
  353.       var splitStrings = input.Split(',');
  354.       var in0 = splitStrings[0];
  355.       var inputUpper = input.ToUpperInvariant();
  356.  
  357.       if (inputUpper.All(c => c >= 'A' && c <= 'Z'))
  358.       {
  359.         var value = stationCodeTable.Decode(inputUpper, true);
  360.         if (!(value is InvalidCodeSentinel) && !value.IsWithinStandardArea())
  361.           Console.WriteLine(InvalidCodeReason.OutsideArea);
  362.         Console.WriteLine(value.FullInfo);
  363.       }
  364.       else if (in0.EndsWith(".txt") || splitStrings.All(s => s.All(c => c >= 'A' && c <= 'Z' || c == '?')))
  365.       {
  366.         Dictionary<InvalidCodeReason, List<string>> InvalidCodes = new Dictionary<InvalidCodeReason, List<string>>();
  367.  
  368.         Dictionary<string, HashSet<string>> namesForGridRef = new Dictionary<string, HashSet<string>>();
  369.  
  370.         List<string> urlFragments = new List<string>();
  371.  
  372.         foreach (var pair in in0.EndsWith(".txt") ? stationCodeTable.GetCodesFromFile(in0, (splitStrings.Length >=2) ? splitStrings[1] : null)
  373.                                                   : stationCodeTable.GetCodes(splitStrings))
  374.         {
  375.           // now only done for VALID codes
  376.           // Console.WriteLine($"{pair.Key} : {pair.Value}");
  377.  
  378.           InvalidCodeReason? errorCode = null;
  379.           if (pair.Value is InvalidCodeSentinel sentinel)
  380.             errorCode = sentinel.Reason;
  381.           else if (!pair.Value.IsWithinStandardArea())
  382.             errorCode = InvalidCodeReason.OutsideArea;
  383.           else
  384.           {
  385.             Console.WriteLine($"{pair.Key} : {pair.Value}");
  386.             string gridRef = pair.Value.GetGridRef();
  387.             if (!namesForGridRef.TryGetValue(gridRef, out var list))
  388.               namesForGridRef.Add(gridRef, list = new HashSet<string>());
  389.             list.Add(pair.Key);
  390.           }
  391.           if (errorCode.HasValue)
  392.           {
  393.             if (!InvalidCodes.TryGetValue(errorCode.Value, out var list))
  394.               InvalidCodes.Add(errorCode.Value, list = new List<string>());
  395.             list.Add(pair.Key);
  396.           }
  397.  
  398.         }
  399.  
  400.         foreach (var pair in InvalidCodes)
  401.           Console.WriteLine($"{pair.Key}: {string.Join(" ", pair.Value)}");
  402.  
  403.         Console.WriteLine($"Drawing map for: " + string.Join(" ", namesForGridRef.Values.Select(set => string.Join("/", set))));
  404.  
  405.         foreach (var pair in namesForGridRef)
  406.           urlFragments.Add($"{pair.Key}|{string.Join("/", pair.Value)}|1");
  407.  
  408.         Console.WriteLine($"https://gridreferencefinder.com/?gr={string.Join(",", urlFragments)}&v=r&labels=1");
  409.         Console.WriteLine($"https://gridreferencefinder.com/osm/?gr={string.Join(",", urlFragments)}&v=r&labels=1");
  410.       }
  411.       else if (splitStrings.Length >= 2 && int.TryParse(splitStrings[0], out int x) && int.TryParse(splitStrings[1], out int y))
  412.       {
  413.         if (splitStrings.Length < 3 || !int.TryParse(splitStrings[2], out int resolution))
  414.           resolution = 50;
  415.         if (splitStrings.Length < 4 || !int.TryParse(splitStrings[3], out int option))
  416.           option = 1;
  417.         Console.WriteLine(stationCodeTable.Encode(x, y, resolution, option));
  418.       }
  419.       else if (in0.Length > 5 && in0.Length % 2 == 0 && char.IsLetter(in0[0]) && char.IsLetter(in0[1]) && in0.Substring(2).All(char.IsNumber) &&
  420.                in0.ParseGridRef() is var gridRef)
  421.       {
  422.         if (splitStrings.Length < 2 || !int.TryParse(splitStrings[1], out int resolution))
  423.           resolution = 50;
  424.         if (splitStrings.Length < 3 || !int.TryParse(splitStrings[2], out int option))
  425.           option = 1;
  426.         Console.WriteLine(stationCodeTable.Encode((int)gridRef.X, (int)gridRef.Y, resolution, option));
  427.       }
  428.     }
  429.   }
  430.  
  431.   public static IEnumerable<string> ReadFileLines(string inputFile)
  432.   {
  433.     using (var file = new StreamReader(inputFile))
  434.     {
  435.       while (!file.EndOfStream)
  436.         yield return file.ReadLine();
  437.     }
  438.   }
  439.  
  440.   public StationCodeTable(string inputFile)
  441.   {
  442.     SingleCodes = new Dictionary<string, StationCode>(2632);
  443.     AmbiguousCodes = new HashSet<string>() { "SHT", "YYZ" };
  444.     ShortCodes = new Dictionary<string, StationCode>(26 * 26);
  445.     foreach(string line in ReadFileLines(inputFile))
  446.     {
  447.       if (line.StartsWith("\"AtcoCode\",\"TiplocCode\",\"CrsCode\""))
  448.         StationCode.InitFields(line);
  449.       else if (line.Length > 4)
  450.       {
  451.         var newCode = new StationCode(line);
  452.         if (!AmbiguousCodes.Contains(newCode.Code))
  453.         {
  454.           string shortCode = newCode.Code.Substring(0, 2);
  455.           if (SingleCodes.TryGetValue(newCode.Code, out var oldCode))
  456.           {
  457.             if (oldCode.X != newCode.X || oldCode.Y != newCode.Y)
  458.             {
  459.               AmbiguousCodes.Add(newCode.Code);
  460.               SingleCodes.Remove(newCode.Code);
  461.               ShortCodes[shortCode] = null;
  462.             }
  463.             else
  464.               Console.WriteLine($"Exact duplicates:\n[{oldCode}] and\n[{newCode}]");
  465.           }
  466.           else
  467.           {
  468.             SingleCodes.Add(newCode.Code, newCode);
  469.             ShortCodes[shortCode] = ShortCodes.TryGetValue(shortCode, out _) ? null : newCode;
  470.           }
  471.         }
  472.       }
  473.     }
  474.     Console.WriteLine($"\n{SingleCodes.Count} lines read");
  475.  
  476.     Console.WriteLine("Short codes:");
  477.     foreach(var pair in ShortCodes.OrderBy(pair => pair.Key).Where(pair => pair.Value!=null))
  478.       Console.WriteLine($"{pair.Key}: {pair.Value}");
  479.  
  480.     Console.WriteLine("Ambiguous 3-codes: "+string.Join(",", AmbiguousCodes));
  481.  
  482.     var codes = SingleCodes.Keys.ToList();
  483.  
  484.     double minDist2 = double.MaxValue;
  485.     for (int i = 0; i < SingleCodes.Count; i++)
  486.     {
  487.       StationCode a = SingleCodes[codes[i]];
  488.       for (int j = i; j < SingleCodes.Count; j++)
  489.       {
  490.         StationCode b = SingleCodes[codes[j]];
  491.         double distAB2 = a.GetDist2(b);
  492.         if (j == i)
  493.           ProcessCode(a);
  494.         else if (distAB2 > 10000) // don't generate a code2 if the originals were within 100m.
  495.         {
  496.           var code = new StationCode2(a, b);
  497.           ProcessCode(code);
  498.           minDist2 = Math.Min(minDist2, distAB2);
  499.         }
  500.       }
  501.     }
  502.     Console.WriteLine($"Done first pass. {CodesProcessed} codes processed, {Duplicates} duplicates");
  503.   }
  504.  
  505.   Dictionary<string, StationCode> SingleCodes { get; }
  506.  
  507.   Dictionary<string, StationCode> ShortCodes { get; }
  508.  
  509.   Dictionary<string, IStationCode> LoadedFileCodes { get; set; }
  510.  
  511.   HashSet<string> AmbiguousCodes { get; }
  512.  
  513.   int CodesProcessed = 0;
  514.   int Duplicates = 0;
  515.  
  516.   const int GridResolution = 100;
  517.  
  518.   // AllCodes contains only 1 and 2 part codes.
  519.   // 3 part codes are too numerous, so are generated dynamically.
  520.   Dictionary<int, Dictionary<int, List<IStationCode>>> AllCodes { get; } = new Dictionary<int, Dictionary<int, List<IStationCode>>>(10000);
  521.  
  522.   void ProcessCode(IStationCode code)
  523.   {
  524.     int boxX = (int)Math.Floor(code.X / GridResolution);
  525.     int boxY = (int)Math.Floor(code.Y / GridResolution);
  526.  
  527.     if (!AllCodes.TryGetValue(boxY, out var rowDict))
  528.       AllCodes.Add(boxY, rowDict = new Dictionary<int, List<IStationCode>>(5000));
  529.  
  530.     if (rowDict.TryGetValue(boxX, out var codeList))
  531.       Duplicates++;
  532.     else
  533.       rowDict.Add(boxX, codeList = new List<IStationCode>());
  534.  
  535.     codeList.Add(code);
  536.     CodesProcessed++;
  537.   }
  538.  
  539.   public void Export(string fileName)
  540.   {
  541.     using (var file = new StreamWriter(fileName))
  542.     {
  543.       foreach (var rowPair in AllCodes)
  544.       {
  545.         int y = rowPair.Key;
  546.         foreach(var pair in rowPair.Value)
  547.         {
  548.           file.WriteLine($"{pair.Key},{y},{string.Join("/",pair.Value.Select(c => c.Code))}");
  549.         }
  550.       }
  551.     }
  552.     Console.WriteLine("Exported to "+fileName);
  553.   }
  554.  
  555.   Dictionary<string, IStationCode> GetCodesFromFile(string inputFile, string conditionText = null)
  556.   {
  557.     return GetCodes(ReadFileLines(inputFile), conditionText);
  558.   }
  559.   Dictionary<string, IStationCode> GetCodes(IEnumerable<string> inputLines, string conditionText = null)
  560.   {
  561.     Func<string, bool> condition = s => true;
  562.     if (conditionText != null)
  563.     {
  564.       if (int.TryParse(conditionText, out int codeLength))
  565.         condition = s => s.Length == codeLength;
  566.       else if (conditionText == "*")
  567.         condition = s => s.Length == 6 &&
  568.                          ShortCodes.TryGetValue(s.Substring(0, 2), out var short1) && short1 != null &&
  569.                          ShortCodes.TryGetValue(s.Substring(2, 2), out var short2) && short2 != null &&
  570.                          ShortCodes.TryGetValue(s.Substring(4, 2), out var short3) && short3 != null;
  571.       else if (conditionText.All(char.IsLetter) && conditionText.ToUpperInvariant() is string upperConditionText)
  572.         condition = s => s.Contains(upperConditionText);
  573.     }
  574.     var result = new Dictionary<string, IStationCode>();
  575.     var iso8859_8 = Encoding.GetEncoding("ISO-8859-8");
  576.     foreach (string line in inputLines)
  577.     {
  578.       if (line.Length > 20)
  579.         Console.WriteLine("Long line ignored: " + line);
  580.       else
  581.       {
  582.         var bytes = iso8859_8.GetBytes(line);
  583.         string normalisedLine = Encoding.UTF8.GetString(bytes).ToUpperInvariant();
  584.         Stack<string> linesToTry = new Stack<string>();
  585.         linesToTry.Push(normalisedLine);
  586.         while (linesToTry.Count > 0)
  587.         {
  588.           string code = "";
  589.           normalisedLine = linesToTry.Pop();
  590.           for (int i = 0; i < normalisedLine.Length; i++)
  591.           {
  592.             char c = normalisedLine[i];
  593.             if (c >= 'A' && c <= 'Z')
  594.               code += c;
  595.             else if(c == '?')
  596.             {
  597.               for (c = 'Z'; c > 'A'; --c)
  598.                 linesToTry.Push(code + c + normalisedLine.Substring(i + 1));
  599.               code += 'A';
  600.             }
  601.           }
  602.           if (condition(code) && !result.ContainsKey(code))
  603.             result.Add(code, Decode(code));
  604.         }
  605.       }
  606.     }
  607.     LoadedFileCodes = result.Where(pair => !(pair.Value is InvalidCodeSentinel) && pair.Value.IsWithinStandardArea()).ToDictionary(pair => pair.Key, pair => pair.Value);
  608.     return result;
  609.   }
  610.  
  611.   string Multi3CodeMessage(string ambiguous2code)
  612.   {
  613.     return "Multiple 3-codes: " + string.Join(" ", SingleCodes.Keys.Concat(AmbiguousCodes).Where(s => s.StartsWith(ambiguous2code)));
  614.   }
  615.  
  616.   string Disambiguate(string original, Action<string> ambiguousAction = null)
  617.   {
  618.     // if ambiguous we return string.Empty.
  619.     // if completely invalid, return the original string so that Disambiguate can return "Not Found".
  620.     // Otherwise, all codes are expanded to the canonical 3-character form.
  621.  
  622.     bool actionDone = false;
  623.     Action<string> recursiveAction = null;
  624.     if (ambiguousAction != null)
  625.       recursiveAction = s => { actionDone = true; ambiguousAction(s); };
  626.  
  627.     switch (original.Length)
  628.     {
  629.       case 2: // Many 2-codes are ambiguous...
  630.         if (ShortCodes.TryGetValue(original, out var info))
  631.         {
  632.           if (info?.Code == null)
  633.             ambiguousAction?.Invoke(Multi3CodeMessage(original));
  634.           return info?.Code ?? string.Empty;
  635.         }
  636.         goto default; // will be "not found".
  637.       case 3:
  638.         if (AmbiguousCodes.Contains(original))
  639.         {
  640.           ambiguousAction?.Invoke($"Ambiguous 3-code [{original}]");
  641.           return string.Empty;
  642.         }
  643.         goto default; // either found as normal, or not found.
  644.       case 4: // must be split as two 2-codes
  645.         {
  646.           if (ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) &&
  647.               ShortCodes.TryGetValue(original.Substring(2, 2), out var info2))
  648.           {
  649.             ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : 2, 2)));
  650.             return (info1 == null || info2 == null) ? string.Empty : info1.Code + info2.Code;
  651.           }
  652.         }
  653.         goto default; // will be "not found".
  654.       case 5: // can be 2-code followed by 3-code or 3-code followed by 2-code.
  655.         {
  656.           string disambiguatedLongEnd = Disambiguate(original.Substring(2, 3), recursiveAction);
  657.           bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) && (disambiguatedLongEnd == string.Empty || SingleCodes.ContainsKey(disambiguatedLongEnd));
  658.           string disambiguatedLongStart = Disambiguate(original.Substring(0, 3), recursiveAction);
  659.           bool shortEnd = ShortCodes.TryGetValue(original.Substring(3, 2), out var info2) && (disambiguatedLongStart == string.Empty || SingleCodes.ContainsKey(disambiguatedLongStart));
  660.           if (shortStart && shortEnd)
  661.           {
  662.             ambiguousAction?.Invoke("Can be split as 2+3 or 3+2"); // most relevant - overrides ambiguous 3-code from recursiveAction
  663.             return string.Empty;
  664.           }
  665.           else if (shortStart && (info1 == null || disambiguatedLongEnd == string.Empty) || shortEnd && (info2 == null || disambiguatedLongStart == string.Empty))
  666.           {
  667.             if (!actionDone)
  668.               ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : 3, 2)));
  669.             return string.Empty;
  670.           }
  671.           if (shortStart)
  672.             return info1.Code + disambiguatedLongEnd;
  673.           if (shortEnd)
  674.             return disambiguatedLongStart + info2.Code;
  675.         }
  676.         goto default;
  677.       case 6: // can be 2 3-codes or 3 2-codes
  678.         {
  679.           bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
  680.           bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(3, 3));
  681.  
  682.           // Without the following early exit, too many 6-letter codes come up "ambiguous".
  683.           // It considers 2+2+2 codes ONLY if the 3+3 code is not valid.
  684.           // (this is reason for "hooks into the NotFound error handler" clue in published puzzle
  685.           // other codes are only explored if the original algorithm that deals only with 6 codes fails).
  686.           if (unambiguous1 && unambiguous2)
  687.             goto default;
  688.  
  689.           bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
  690.           bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(3, 3));
  691.  
  692.           if (ambiguous1 && (ambiguous2 || unambiguous2) || unambiguous1 && ambiguous2)
  693.           {
  694.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 3, 3)}]");
  695.             return string.Empty; // ambiguous before we even consider short codes.
  696.           }
  697.  
  698.           // else either there's no valid 3+3 code, or it would, in itself, be unambiguous.
  699.           // check for a 2+2+2 code.
  700.  
  701.           if (ShortCodes.TryGetValue(original.Substring(0, 2), out var info1) &&
  702.               ShortCodes.TryGetValue(original.Substring(2, 2), out var info2) &&
  703.               ShortCodes.TryGetValue(original.Substring(4, 2), out var info3))
  704.           {
  705.             // the following if() is now redundant, as we always favour a valid
  706.             // 6 letter code when it exists.
  707.             if (unambiguous1 && unambiguous2)
  708.             {
  709.               ambiguousAction?.Invoke("Can be split as 3+3 or 2+2+2"); // most relevant - overrides ambiguous 2-codes
  710.               return string.Empty;
  711.             }
  712.             if (info1 == null || info2 == null || info3 == null)
  713.             {
  714.               ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(info1 == null ? 0 : info2 == null ? 2 : 4, 2)));
  715.               return string.Empty;
  716.             }
  717.             return info1.Code + info2.Code + info3.Code;
  718.           }
  719.         }
  720.         goto default;
  721.  
  722.       case 7: // must be 1 3-code and 2 2-codes. 3-code can be in one of 3 positions.
  723.         {
  724.           bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
  725.           bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(2, 3));
  726.           bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(4, 3));
  727.           // in this case, shortStart etc. say nothing of whether they form part of a valid code.
  728.           bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var infoStart);
  729.           bool shortMid1 = ShortCodes.TryGetValue(original.Substring(2, 2), out var infoMid1);
  730.           bool shortMid2 = ShortCodes.TryGetValue(original.Substring(3, 2), out var infoMid2);
  731.           bool shortEnd = ShortCodes.TryGetValue(original.Substring(5, 2), out var infoEnd);
  732.  
  733.           if (ambiguous1 && shortMid2 && shortEnd)
  734.           {
  735.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(0, 3)}]");
  736.             return string.Empty; // an ambiguous 3-code is valid in position 1.
  737.           }
  738.           if (shortStart && ambiguous2 && shortEnd)
  739.           {
  740.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(2, 3)}]");
  741.             return string.Empty; // an ambiguous 3-code is valid in position 2.
  742.           }
  743.           if (shortStart && shortMid1 && ambiguous3)
  744.           {
  745.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(4, 3)}]");
  746.             return string.Empty; // an ambiguous 3-code is valid in position 3.
  747.           }
  748.  
  749.           bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
  750.           bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(2, 3));
  751.           bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(4, 3));
  752.  
  753.           string expanded = null;
  754.           if (unambiguous1 && shortMid2 && shortEnd)
  755.           {
  756.             if (infoMid2 == null || infoEnd == null)
  757.             {
  758.               ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoMid2 == null ? 3 : 5, 2)));
  759.               return string.Empty;
  760.             }
  761.             expanded = original.Substring(0, 3) + infoMid2.Code + infoEnd.Code;
  762.           }
  763.           if (shortStart && unambiguous2 && shortEnd)
  764.           {
  765.             if (infoStart == null || infoEnd == null)
  766.             {
  767.               ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoStart == null ? 0 : 5, 2)));
  768.               return string.Empty;
  769.             }
  770.             if(expanded!=null)
  771.             {
  772.               ambiguousAction?.Invoke($"Can be interpreted as {expanded} or {infoStart.Code + original.Substring(2, 3) + infoEnd.Code}");
  773.               return string.Empty;
  774.             }
  775.             expanded = infoStart.Code + original.Substring(2, 3) + infoEnd.Code;
  776.           }
  777.           if (shortStart && shortMid1 && unambiguous3)
  778.           {
  779.             if (infoStart == null || infoMid1 == null)
  780.             {
  781.               ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(infoMid2 == null ? 0 : 2, 2)));
  782.               return string.Empty;
  783.             }
  784.             if (expanded != null)
  785.             {
  786.               ambiguousAction?.Invoke($"Can be interpreted as {expanded} or {infoStart.Code + infoMid1.Code + original.Substring(4, 3)}");
  787.               return string.Empty;
  788.             }
  789.             expanded = infoStart.Code + infoMid1.Code + original.Substring(4, 3);
  790.           }
  791.           return expanded ?? original;
  792.         }
  793.  
  794.       case 8: // must be 2 3-codes and a 1-code. 2-code can be in one of 3 positions.
  795.         {
  796.           bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
  797.           bool ambiguous2a = AmbiguousCodes.Contains(original.Substring(2, 3));
  798.           bool ambiguous2b = AmbiguousCodes.Contains(original.Substring(3, 3));
  799.           bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(5, 3));
  800.           bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
  801.           bool unambiguous2a = SingleCodes.ContainsKey(original.Substring(2, 3));
  802.           bool unambiguous2b = SingleCodes.ContainsKey(original.Substring(3, 3));
  803.           bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(5, 3));
  804.           // in this case, shortStart etc. are only set if the 3-codes are valid (albeit potentially ambiguous)
  805.           bool shortStart = ShortCodes.TryGetValue(original.Substring(0, 2), out var infoStart) && (ambiguous2a || unambiguous2a) && (ambiguous3 || unambiguous3);
  806.           bool shortMid = ShortCodes.TryGetValue(original.Substring(3, 2), out var infoMid) && (ambiguous1 || unambiguous1) && (ambiguous3 || unambiguous3);
  807.           bool shortEnd = ShortCodes.TryGetValue(original.Substring(6, 2), out var infoEnd) && (ambiguous1 || unambiguous1) && (ambiguous2b || unambiguous2b);
  808.  
  809.           if (shortStart && (shortMid || shortEnd) || shortMid && shortEnd)
  810.           {
  811.             ambiguousAction?.Invoke("Can be interpreted as" + (shortStart ? "  2+3+3" : "") + (shortMid ? "  3+2+3" : "") + (shortEnd ? "  3+3+2" : ""));
  812.             return string.Empty;
  813.           }
  814.  
  815.           // at most one of shortStart, shortMid or shortEnd is set now.
  816.           if (!shortStart && !shortMid && !shortEnd)
  817.             return original; // will cause "NotFound" error as length is not valid for caller.
  818.  
  819.           // exactly one of shortStart, shortMid or ShortEnd is set now.
  820.           if (shortStart && (ambiguous2a || ambiguous3))
  821.           {
  822.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous2a ? 2 : 5, 3)}]");
  823.             return string.Empty;
  824.           }
  825.           else if(shortMid && (ambiguous1 || ambiguous3))
  826.           {
  827.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 5, 3)}]");
  828.             return string.Empty;
  829.           }
  830.           else if(shortEnd && (ambiguous1 || ambiguous2b))
  831.           {
  832.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : 3, 3)}]");
  833.             return string.Empty;
  834.           }
  835.  
  836.           if ((shortStart ? infoStart : shortMid ? infoMid : infoEnd) == null)
  837.           {
  838.             ambiguousAction?.Invoke(Multi3CodeMessage(original.Substring(shortStart ? 0 : shortMid ? 3 : 6, 2)));
  839.             return string.Empty;
  840.           }
  841.  
  842.           return shortStart ? infoStart.Code + original.Substring(2, 6) :
  843.                  shortMid ? original.Substring(0, 3) + infoMid.Code + original.Substring(5, 3) :
  844.                  shortEnd ? original.Substring(0, 6) + infoEnd.Code :
  845.                  throw new InvalidOperationException("At least one must be set at this point in code!");
  846.         }
  847.  
  848.       case 9: // if it's not ambiguous, it's already in canonical form.
  849.         {
  850.           bool unambiguous1 = SingleCodes.ContainsKey(original.Substring(0, 3));
  851.           bool unambiguous2 = SingleCodes.ContainsKey(original.Substring(3, 3));
  852.           bool unambiguous3 = SingleCodes.ContainsKey(original.Substring(6, 3));
  853.           bool ambiguous1 = AmbiguousCodes.Contains(original.Substring(0, 3));
  854.           bool ambiguous2 = AmbiguousCodes.Contains(original.Substring(3, 3));
  855.           bool ambiguous3 = AmbiguousCodes.Contains(original.Substring(6, 3));
  856.           if (ambiguous1 && (ambiguous2 || unambiguous2) && (ambiguous3 || unambiguous3) ||
  857.               unambiguous1 && (ambiguous2 && (ambiguous3 || unambiguous3) || unambiguous2 && ambiguous3))
  858.           {
  859.             ambiguousAction?.Invoke($"Ambiguous 3-code [{original.Substring(ambiguous1 ? 0 : ambiguous2 ? 6 : 9, 3)}]");
  860.             return string.Empty;
  861.           }
  862.         }
  863.         goto default;
  864.  
  865.       default: return original;
  866.     }
  867.   }
  868.  
  869.   IStationCode Decode(string codeString, bool explainAmbiguities = false)
  870.   {
  871.     string ambiguousReason = null;
  872.     codeString = explainAmbiguities ? Disambiguate(codeString, s => ambiguousReason = s) : Disambiguate(codeString);
  873.     if (codeString.Length > 9)
  874.       return (InvalidCodeSentinel)InvalidCodeReason.TooLong;
  875.     switch (codeString.Length)
  876.     {
  877.       case 0:
  878.         if (!string.IsNullOrEmpty(ambiguousReason))
  879.           return new InvalidCodeSentinel(InvalidCodeReason.Ambiguous, ambiguousReason);
  880.         return (InvalidCodeSentinel)InvalidCodeReason.Ambiguous;
  881.       case 3:
  882.         if (SingleCodes.TryGetValue(codeString, out var stationCode))
  883.           return stationCode;
  884.         break;
  885.  
  886.       case 6:
  887.         if (SingleCodes.TryGetValue(codeString.Substring(0, 3), out var codeA))
  888.           if (SingleCodes.TryGetValue(codeString.Substring(3, 3), out var codeB))
  889.             return new StationCode2(codeA, codeB);
  890.         break;
  891.       case 9:
  892.         if (SingleCodes.TryGetValue(codeString.Substring(0, 3), out var a))
  893.           if (SingleCodes.TryGetValue(codeString.Substring(3, 3), out var b))
  894.             if (SingleCodes.TryGetValue(codeString.Substring(6, 3), out var c))
  895.             {
  896.               double minDist2 = Math.Min(a.GetDist2(b), a.GetDist2(c));
  897.               minDist2 = Math.Min(minDist2, b.GetDist2(c));
  898.  
  899.               var code = new StationCode3(a, b, c);
  900.  
  901.               if (a.GetDist2(code) > 100 * minDist2)
  902.                 Console.WriteLine("WARNING: Badly formed");
  903.  
  904.  
  905.               return code;
  906.  
  907.             }
  908.         break;
  909.     }
  910.     return (InvalidCodeSentinel)InvalidCodeReason.NotFound;
  911.   }
  912.  
  913.   string Encode(int x, int y, int resolution = 50, int option = 1)
  914.   {
  915.     bool include3codes = option == 1 || option > 5;
  916.  
  917.     int minBoxX = (x - resolution) / GridResolution;
  918.     int maxBoxX = (x + resolution) / GridResolution;
  919.     int minBoxY = (y - resolution) / GridResolution;
  920.     int maxBoxY = (y + resolution) / GridResolution;
  921.  
  922.     List<Tuple<IStationCode, double>> codes = new List<Tuple<IStationCode, double>>();
  923.  
  924.     // ignore distance parameter to find nearby codes from last processed file
  925.  
  926.     var fileCodesByDistance = LoadedFileCodes?.Values.OrderBy(code => code.GetDist2(x, y)).Take(3) ?? Enumerable.Empty<IStationCode>();
  927.  
  928.     foreach (var code in fileCodesByDistance)
  929.       codes.Add(Tuple.Create(code, code.GetDist2(x, y)));
  930.  
  931.     int resolution2 = resolution * resolution;
  932.  
  933.     for (int boxY = minBoxY; boxY <= maxBoxY; boxY++)
  934.       if (AllCodes.TryGetValue(boxY, out var rowDict))
  935.         for (int boxX = minBoxX; boxX <= maxBoxX; boxX++)
  936.           if (rowDict.TryGetValue(boxX, out var codeList))
  937.             codes.AddRange(codeList.Select(code => Tuple.Create(code, code.GetDist2(x, y))).Where(t => t.Item2 <= resolution2));
  938.  
  939.     if (include3codes)
  940.     {
  941.       Console.WriteLine("Including 3-codes...");
  942.  
  943.       var targetsByDistance = SingleCodes.Values.OrderBy(sc => sc.GetDist2(x, y)).ToList();
  944.       double prevDist = 0;
  945.       double prev2Dist = 0;
  946.       for (int i = 0; i < targetsByDistance.Count; i++)
  947.       {
  948.         double myDist = Math.Sqrt(targetsByDistance[i].GetDist2(x, y));
  949.         double distAB2, distAC2, distBC2;
  950.         if (i >= 2 && prev2Dist > myDist - resolution &&
  951.             myDist * prev2Dist / 100 < (distAB2 = targetsByDistance[i - 2].GetDist2(targetsByDistance[i - 1])) &&
  952.             myDist * prev2Dist / 100 < (distAC2 = targetsByDistance[i - 2].GetDist2(targetsByDistance[i])) &&
  953.             myDist * prev2Dist / 100 < (distBC2 = targetsByDistance[i - 1].GetDist2(targetsByDistance[i])))
  954.         {
  955.           IStationCode code = new StationCode3(targetsByDistance[i - 2], targetsByDistance[i - 1], targetsByDistance[i], distAB2, distAC2);
  956.           double dist2 = code.GetDist2(x, y);
  957.           if (dist2 < resolution2)
  958.             codes.Add(Tuple.Create(code, dist2));
  959.         }
  960.  
  961.         prev2Dist = prevDist;
  962.         prevDist = myDist;
  963.       }
  964.     }
  965.     var filteredCodes = option < 2 ? codes :
  966.                         codes.Where(t => t.Item1.Code.Length == option || GetEquivalentCodes(t.Item1.Code).Any(s => s.Length == option));
  967.     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))}"));
  968.   }
  969.  
  970.   IEnumerable<string> GetEquivalentCodes(string codeString)
  971.   {
  972.     if (codeString.Length % 3 != 0 || codeString.Length == 0)
  973.       throw new ArgumentOutOfRangeException(codeString);
  974.     if (codeString.Length == 3)
  975.     {
  976.       string shortCode = codeString.Substring(0, 2);
  977.       if (ShortCodes.TryGetValue(shortCode, out var codeInfo) && codeInfo != null)
  978.         return new[] { codeString, shortCode };
  979.       else if (SingleCodes.ContainsKey(codeString))
  980.         return new[] { codeString };
  981.       else
  982.         return Enumerable.Empty<string>();
  983.     }
  984.     HashSet<string> LongCodes = new HashSet<string>();
  985.     for (int i = 0; i < codeString.Length; i += 3)
  986.       if (!LongCodes.Add(codeString.Substring(i, 3)))
  987.         return Enumerable.Empty<string>();
  988.  
  989.     List<string> results = new List<string>();
  990.     foreach(var longCode in LongCodes)
  991.     {
  992.       IEnumerable<string> longCodeForms = GetEquivalentCodes(longCode);
  993.       IEnumerable<string> remainderForms = GetEquivalentCodes(string.Join("", LongCodes.Where(c => c != longCode)));
  994.       foreach (var prefix in longCodeForms)
  995.         foreach (var suffix in remainderForms)
  996.           if (Disambiguate(prefix + suffix) != string.Empty)
  997.             results.Add(prefix + suffix);
  998.     }
  999.     return results;
  1000.   }
  1001.  
  1002.   static IEnumerable<string> GenerateAllStrings(int length)
  1003.   {
  1004.     if (length == 0)
  1005.       yield return string.Empty;
  1006.     else
  1007.       foreach (var shorterString in GenerateAllStrings(length - 1))
  1008.         for (char addedChar = 'A'; addedChar <= 'Z'; addedChar++)
  1009.           yield return shorterString + addedChar;
  1010.   }
  1011.  
  1012.   static IEnumerable<string> GenerateRandomStrings(int length)
  1013.   {
  1014.     Random rand = new Random();
  1015.     char[] charArray = new char[length];
  1016.     while(true)
  1017.     {
  1018.       for (int i = 0; i < length; i++)
  1019.         charArray[i] = (char)rand.Next('A', 'Z');
  1020.       yield return new string(charArray);
  1021.     }
  1022.   }
  1023.  
  1024.   public void MonteCarlo(int length, int count)
  1025.   {
  1026.     double allValid = Math.Pow(26, length);
  1027.     IEnumerable<string> stringsToCheck;
  1028.     if (count > allValid * 0.99)
  1029.     {
  1030.       count = (int)Math.Round(allValid);
  1031.       stringsToCheck = GenerateAllStrings(length);
  1032.     }
  1033.     else
  1034.       stringsToCheck = GenerateRandomStrings(length).Distinct().Take(count);
  1035.     int totalAmbiguous = 0;
  1036.     int totalUnknown = 0;
  1037.     int totalOutsideArea = 0;
  1038.     int totalValid = 0;
  1039.     Console.WriteLine($"Running Monte-Carlo analysis for {count} strings of length {length}...");
  1040.     foreach(var testString in stringsToCheck)
  1041.     {
  1042.       var code = Decode(testString);
  1043.       switch ((code as InvalidCodeSentinel )?.Reason)
  1044.       {
  1045.         case InvalidCodeReason.NotFound: totalUnknown++; break;
  1046.         case InvalidCodeReason.Ambiguous: totalAmbiguous++; break;
  1047.         case null:
  1048.           if (code.IsWithinStandardArea())
  1049.             totalValid++;
  1050.           else
  1051.             totalOutsideArea++;
  1052.           break;
  1053.         default: throw new NotImplementedException();
  1054.       }
  1055.     }
  1056.     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");
  1057.   }
  1058.  
  1059. }
  1060.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement