[C#] [SourceCode] Römische Zahlen umrechnen

    • C#

    Es gibt 36 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      [C#] [SourceCode] Römische Zahlen umrechnen

      Mir war gerade ziemlich langweilig, und deswegen hab ich mir einfach mal den Spaß erlaubt nen Algo zum Umrechnen von und nach römischen Zahlen zu implementieren.
      Ich werd jetzt nichts mehr speziell dazu sagen, da ich den Quelltext ziemlich ausführlich kommentiert hab, so dass alle meine Gedankengänge ersichtlich werden sollten.
      Es handelt sich im Groben einfach um eine Struktur, die römische zahlen speichert. Intern wird ein Integer verwendet und die eigentliche Funktionalität liegt nur in der TryParse- und der ToString-Funktion, den Rest hab ich auch nicht kommentiert (sind eh bloß ein paar Operatoren).
      Vielleicht kann ja jemand irgend was damit anfangen oder wenigstens was daraus lernen.

      Spoiler anzeigen

      C#-Quellcode

      1. public struct RomanNumber
      2. {
      3. static readonly RomanDigit[] Digits;
      4. int value;
      5. private struct RomanDigit
      6. {
      7. public char Digit;
      8. public int Value;
      9. }
      10. static RomanNumber()
      11. {
      12. Digits = new RomanDigit[7];
      13. Digits[0] = new RomanDigit() { Digit = 'M', Value = 1000 };
      14. Digits[1] = new RomanDigit() { Digit = 'D', Value = 500 };
      15. Digits[2] = new RomanDigit() { Digit = 'C', Value = 100 };
      16. Digits[3] = new RomanDigit() { Digit = 'L', Value = 50 };
      17. Digits[4] = new RomanDigit() { Digit = 'X', Value = 10 };
      18. Digits[5] = new RomanDigit() { Digit = 'V', Value = 5 };
      19. Digits[6] = new RomanDigit() { Digit = 'I', Value = 1 };
      20. }
      21. public static bool TryParse(string s, out RomanNumber result)
      22. {
      23. result = default(RomanNumber);
      24. s = s.ToUpperInvariant();
      25. // --------------------------------------------- Regeln ---------------------------------------------
      26. //
      27. // 1. Es dürfen nur bekannte Zeichen (M, D, C, L, X, V, I) im String vorkommen.
      28. //
      29. // 2. Die Zeichen sind in ihrer Wertigkeit von links nach rechts absteigend sortiert.
      30. // -> Zeichen mit niedrigerer Wertigkeit dürfen nicht weiter links als Zeichen
      31. // mit höherer Wertigkeit stehen.
      32. //
      33. // 3. Einzige Ausnahme für 2. ist, wenn das Zeichen zum Subtrahieren verwendet wird (z.B. IV).
      34. //
      35. // 4. Es dürfen nur Zeichen, deren Wertigkeit zu Basis 10 liegt, zum Subtrahieren verwendet werden.
      36. // Ebenso darf ausschließlich die Ziffer mit der nächst kleineren Wertigkeit subtrahiert werden.
      37. //
      38. // 5. Es muss immer die geringst mögliche Anzahl Zeichen verwendet werden.
      39. // -> Zeichen mit niedrigerer Wertigkeit dürfen nur so oft verwendet werden,
      40. // bis man den selben Zahlenwertdurch ein Zeichen mit höherer Wertigkeit erreichen würde.
      41. //
      42. // 6. Ergänzung zu 5.
      43. // -> Die Subtraktionsregel ist hier mit zu beachten, also IV ist IIII vorzuziehen.
      44. //
      45. // 7. Es muss immer zuerst von der kleinstmöglichen Ziffer subtrahiert werden, also XIV
      46. // wird IXV vorgezogen.
      47. //
      48. // --------------------------------------------------------------------------------------------------
      49. int value = 0;
      50. int currentDigitIndex = -1;
      51. int currentDigitCount = 0;
      52. int lowestDigitIndex = -1;
      53. //Hier nehmen wir eine while-Schleife statt einer for-Schleife, da wir später unterschiedlich inkrementieren müssen.
      54. int index = 0;
      55. while (index < s.Length)
      56. {
      57. //Wir brauchen sowohl die Ziffer des aktuellen alsauch des nächsten Zeichens.
      58. int digitIndex = Array.FindIndex(Digits, x => x.Digit == s[index]);
      59. int nextDigitIndex = index < s.Length - 1 ? Array.FindIndex(Digits, x => x.Digit == s[index + 1]) : -1;
      60. if (digitIndex == -1)
      61. //Der Eingabestring ist ungültig, siehe 1.
      62. return false;
      63. //Wir müssen nun zwei Fälle unterscheiden, Subtraktion und Addition.
      64. if ((nextDigitIndex > -1) && (digitIndex > nextDigitIndex))
      65. {
      66. //Hier liegt Subtraktion vor, da die nächste Ziffer eine größere Wertigkeit als die aktuelle besitzt.
      67. if ((digitIndex % 2 == 1) || (nextDigitIndex < digitIndex - 2))
      68. //Der Eingabestring ist ungültig, siehe 4.
      69. return false;
      70. if (nextDigitIndex < lowestDigitIndex)
      71. //Der Eingabestring ist ungültig, siehe 2.
      72. //Wichtig ist hier, dass wir die nächste Ziffer zum Vergleichen verwenden, da die aktuelle
      73. //wegen 3. aus der Regel herausfällt.
      74. return false;
      75. //Wir müssen auch dafür sorgen, dass die aktuelle Ziffer als die mit der niedrigsten Wertigkeit
      76. //festgelegt wird, um weiterhin auf 2. prüfen zu können. Wir zählen allerdings gleich noch 1 dazu, da
      77. //eine solche Konstellation nur genau einmal pro Ziffer erlaubt ist, da ansonsten 5. nicht erfüllt ist.
      78. lowestDigitIndex = nextDigitIndex + 1;
      79. //Um 5. einzuhalten müssen wir auch noch das übernächste Zeichen prüfen, da sonst Konstrukte wie IVI möglich wären.
      80. int behindNextIndex = index < s.Length - 2 ? Array.FindIndex(Digits, x => x.Digit == s[index + 2]) : -1;
      81. if ((behindNextIndex > -1) && (behindNextIndex == digitIndex))
      82. return false; //Hier haben wir einen Verstoß gegen 5.
      83. //Die übernächste Ziffer brauchen wir auch, um auf 7. zu prüfen.
      84. if ((behindNextIndex > -1) && ((behindNextIndex | 1) + 1 == digitIndex))
      85. return false; //Hier haben wir einen Verstoß gegen 7.
      86. //Schließlich zählen wir die Wertigkeit der nächsten Ziffer abzüglich der Wertigkeit der aktuellen Ziffer hinzu.
      87. value += Digits[nextDigitIndex].Value - Digits[digitIndex].Value;
      88. index += 2; //Und wir gehen gleich zwei Zeichen weiter im String, da wir gerade zwei Zeichen bearbeitet haben.
      89. }
      90. else
      91. {
      92. //Hier haben wir die Addition, bzw. keine speziellen Umstände.
      93. if (digitIndex < lowestDigitIndex)
      94. //Hier ist der Eingabestring nach 2. wieder ungültig.
      95. return false;
      96. //Diesmal lassen wir das + 1 weg, da nach einer Addition nicht unbedingt die Ziffernwertigkeit sinken muss,
      97. //anders als eben bei der Subtraktion.
      98. lowestDigitIndex = digitIndex;
      99. //Jetzt müssen wir noch 5. berücksichtigen.
      100. if (currentDigitIndex == digitIndex)
      101. {
      102. //Wir brauchen einen Counter, damit wir berechnen können, ob wir schon zu viele gleiche Ziffern hinterheinander hatten.
      103. currentDigitCount++;
      104. //Hier müssen wir jetzt wegen 6. zwei Fälle unterscheiden.
      105. //Wenn die aktuelle Ziffer eine Hilfsbasis ist, dann brauchen wir nichts speziell zu berücksichteigen,
      106. //andernfalls jedoch müssen wir auf die Subtraktionsregel achten.
      107. if (digitIndex % 2 == 1)
      108. {
      109. //Wenn die Wertigkeit aller Ziffern größer ist, als die der nächst höheren Ziffer, liegt ein Verstoß gegen 5. vor.
      110. if (digitIndex > 0 && Digits[digitIndex].Value * currentDigitCount >= Digits[digitIndex - 1].Value)
      111. return false;
      112. }
      113. else
      114. {
      115. //Hier müssen wir jezt auch noch einmal den Wert der aktuellen Ziffer abziehen, wegen der Subtraktionsregel.
      116. if (digitIndex > 0 && Digits[digitIndex].Value * currentDigitCount >= Digits[digitIndex - 1].Value - Digits[digitIndex].Value)
      117. return false;
      118. }
      119. }
      120. else
      121. {
      122. //Hier sind wir aus dem kritischen Bereich raus, da wir mindestens eine Wertigkeitsstufe runtergegangen sind.
      123. //Wir können also einfach alles wieder zurück auf Start stellen.
      124. currentDigitIndex = digitIndex;
      125. currentDigitCount = 1;
      126. }
      127. //Beim Addieren zählen wir jetzt einfach die Wertigkeit der aktuellen Ziffer dazu.
      128. value += Digits[digitIndex].Value;
      129. index++; //Hier inkrementieren wir auch nur um eins, denn das nächste Zeichen haben wir noch nicht bearbeitet.
      130. }
      131. }
      132. result = value;
      133. return true;
      134. }
      135. public static RomanNumber Parse(string s)
      136. {
      137. RomanNumber result;
      138. if (!TryParse(s, out result))
      139. throw new FormatException();
      140. return result;
      141. }
      142. public override string ToString()
      143. {
      144. //In der umgekehrten Richtung haben wirs wesentlich einfacher, da wir selbstverständlich
      145. //keinerlei mögliche Fehler in der Eingabe überprüfen müssen, wir generierens einfach richtig. ;)
      146. StringBuilder sb = new StringBuilder();
      147. int value = this.value; //Wir müssen uns den Zahlenwert zwischenspeichern, da wir da dran rumpfuschen werden.
      148. //Das Prinzip ist eigentlich recht einfach. Wir gehen alle vorhandenen Ziffern durch, wobei wir
      149. //die mit der größten Wertigkeit zuerst durchlaufen.
      150. for (int i = 0; i < Digits.Length; i++)
      151. {
      152. //Jetzt hängen wir diese Ziffer so lange an die Ausgabe an, bis sie nicht mehr in den Restbetrag reinpasst.
      153. int count = Math.DivRem(value, Digits[i].Value, out value);
      154. sb.Append(Digits[i].Digit, count);
      155. //Wäre da nicht die Subtraktionsregel, wären wir jetzt schon fertig und könnten mit der nächsten Ziffer weitermachen,
      156. //aber diese Regel gibts nunmal.
      157. if (i < Digits.Length - 1)
      158. {
      159. //Wir bestimmen zuerst die Subtraktionsziffer...
      160. int subtractionIndex = (i | 1) + 1;
      161. //...und prüfen dann, ob der Wert mit Subtraktion noch reinpasst.
      162. if (value >= Digits[i].Value - Digits[subtractionIndex].Value)
      163. {
      164. sb.Append(Digits[subtractionIndex].Digit);
      165. sb.Append(Digits[i].Digit);
      166. value -= Digits[i].Value - Digits[subtractionIndex].Value;
      167. }
      168. }
      169. }
      170. return sb.ToString();
      171. }
      172. public static implicit operator RomanNumber(int value)
      173. {
      174. if (value < 0)
      175. throw new OverflowException("Negative Werte sind nicht zulässig");
      176. return new RomanNumber() {value = value};
      177. }
      178. public static implicit operator int(RomanNumber value)
      179. {
      180. return value.value;
      181. }
      182. public static RomanNumber operator +(RomanNumber left, RomanNumber right)
      183. {
      184. return left.value + right.value;
      185. }
      186. public static RomanNumber operator -(RomanNumber left, RomanNumber right)
      187. {
      188. return left.value - right.value;
      189. }
      190. public static RomanNumber operator *(RomanNumber left, RomanNumber right)
      191. {
      192. return left.value * right.value;
      193. }
      194. public static RomanNumber operator /(RomanNumber left, RomanNumber right)
      195. {
      196. return left.value / right.value;
      197. }
      198. public static bool operator ==(RomanNumber left, RomanNumber right)
      199. {
      200. return left.value == right.value;
      201. }
      202. public static bool operator !=(RomanNumber left, RomanNumber right)
      203. {
      204. return left.value != right.value;
      205. }
      206. public static bool operator <(RomanNumber left, RomanNumber right)
      207. {
      208. return left.value < right.value;
      209. }
      210. public static bool operator >(RomanNumber left, RomanNumber right)
      211. {
      212. return left.value > right.value;
      213. }
      214. public static bool operator <=(RomanNumber left, RomanNumber right)
      215. {
      216. return left.value <= right.value;
      217. }
      218. public static bool operator >=(RomanNumber left, RomanNumber right)
      219. {
      220. return left.value >= right.value;
      221. }
      222. }

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Artentus“ ()

      Es wäre mal Interessant zu wissen, was die Vor- und Nachteile von deiner und der vom Löffelman sind. Auf jedenfall geht bei Löffelmans Version IXV nicht. Kannste dir ja mal angucken und ggf. etwas übernehmen ;)
      (Damit, wenn du es dir angucken willst, nicht ewig suchen musst, das Projekt liegt unter Beispieldateien_vb2005.net\D - OOP\Kap08\RomNum_Net04. Downloaden kannst du es dir hier (nicht das Buch downloaden, sondern die Beispieldateien. Steht da alles.).)
      Mfg
      Vincent

      Ich hab mir hier alles selbst ausgedacht, nichts abgeschrieben, das ist ja der Sinn hinter so ner Fingerübung.
      Kann durchaus sein (ist bestimmt so), dass es da schon wesentlich bessere standardisierte Methoden gibt.

      Edti: ich finde auch ehrlich gesagt die Beispielcodes nicht.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Artentus“ ()

      Nein, ich hätte nie gedacht, dass du das abschreibst :)
      Ich dachte eher, dass du vielleicht sogar von ihm etwas lernen könntest :D
      Hm, den Beispielcode finde ich auch nicht mehr im Internet... Hab ihn mal angehängt. Hoffe der hat da nichts gegen.
      Dateien
      • RomNum_Net04.zip

        (14,23 kB, 247 mal heruntergeladen, zuletzt: )
      Mfg
      Vincent

      Also um ehrlich zu sein beeindruckt mich diese Code nicht im mindesten, der sieht für mich eher schlampig aus.
      Ich hab alles so programmiert, dass es beliebig erweiterbar ist (sowohl durch Hinzufügen neuer Zeichen alsauch durch Ändern der Wertigkeit der Zeichen), er hingegen hat da ziemlich viel hardgecodet. Und dann auch noch sowas:

      VB.NET-Quellcode

      1. Private Shared Table(6, 1) As String
      2. Shared Sub New()
      3. Table(0, 0) = "I" : Table(0, 1) = "1"
      4. Table(1, 0) = "V" : Table(1, 1) = "5"
      5. Table(2, 0) = "X" : Table(2, 1) = "10"
      6. Table(3, 0) = "L" : Table(3, 1) = "50"
      7. Table(4, 0) = "C" : Table(4, 1) = "100"
      8. Table(5, 0) = "D" : Table(5, 1) = "500"
      9. Table(6, 0) = "M" : Table(6, 1) = "1000"
      10. End Sub
      XML-Doku für Parse, TryParse und ToString? Naja, wenn mans ganz eng mit den Guidelines sieht muss mans hinzufügen, aber ehrlich gesagt überlese ich in so nem Fall einfach gekonnt den StyleCop-Hinweis. ;)
      Unittest hab ich noch nie erstellt, ich wüsste gar nicht, wie das geht. :whistling:
      hier meine Überarbeitung von ToString:

      C#-Quellcode

      1. public override string ToString() {
      2. StringBuilder sb = new StringBuilder();
      3. int rest = this.Value;
      4. for(int i = 0; i < Digits.Length; i++) {
      5. var additor = Digits[i];
      6. var additorCount = Math.DivRem(rest, additor.Value, out rest); //ganzzahl-teilung mit Rest
      7. sb.Append(new string(additor.Digit,additorCount)); //den additor.Digit so oft anhängen, wie er in rest hineinpasste.
      8. /* additor passt nun nicht mehr in rest hinein.
      9. * Nun einen Subtractor suchen, den man voranstellen kann, um den additor doch noch zu verwenden (meist findet sich keiner)*/
      10. for(int j = (i | 1) + 1; j < Digits.Length; j += 2) {//nur Digits auf graden Indizees können Subtractoren sein
      11. var subtractor = Digits[j];
      12. if(rest >= additor.Value - subtractor.Value) {
      13. sb.Append(subtractor.Digit);
      14. sb.Append(additor.Digit);
      15. rest -= additor.Value - subtractor.Value;
      16. break;
      17. }
      18. }
      19. }
      20. return sb.ToString();
      21. }
      Beachte die schnuckelige DivRem - Methode. Auch das mit den graden Indizees findich listig.

      @nikeee13:: wenn du unzufrieden bist, dann schreib doch die Xml-Doku, und vor allem die UnitTests :P
      Das mit dem DivRem ist tatsächlich eine gute Idee, so spart man sich die Schleife.
      Mir ist auch beim Programmieren durchaus aufgefallen, dass nur jede zweite Ziffer zum Subtrahieren verwendet werden kann, und anfangs hab ich auch damit rumprobiert. Ich habs dann letzendlich aber doch mit diesem Boolean-Flag gelöst, da man so flexibler ist, man könnte bei meinem Code oben ja die Flag beliebig ändern, theoretisch kann man auch neue Zeichen da einfügen. Ist im Falle der römischen Zahlensystems zwar eigentlich Sinnlos, da wird sich nie mehr was dran ändern, aber ich schreib sowas ja aus Übungszwecken, und da will ich auch die maximale Erweiterbarkeit haben.
      offTopic:
      Ist mir jetzt erst klargeworden: die Römer waren garnet so blöd, und in gewisser Weise vom 10er - Stellenwert-System garnicht so weit entfernt, odr?
      also die Hauptreihe 1, 10, 100, 1000 - hat schon was.


      Ich weiß auchnicht, warum ich immer dachte, die Römerzahlen wären iwie zwölf-basiert, oder die XII hätte iwie ein besonderes Gewicht in deren System. Hatse ja garnet.
      Das mitte XII kommt vonne Zeit-Einteilung, und die ist glaub älter als die Römer.
      ich fragte mich grade: Wie schreibt man eiglich 99 auf römisch: XCIX oder IC?
      Antwort fund ich auf Wiki, nämlich XCIX.
      Das brachte mich drauf, dass die Subtraktions-Regel bisserl anners ist, als bei dir formuliert, und v.a., dass in ToString keine innere schleife hineingehört, sondern nur ein if:

      C#-Quellcode

      1. static RomanNumber() {
      2. Digits = new RomanDigit[9];// array bewust um 2 zu groß gewählt, um im Tostring-Algo auf Array-Grenz-Überprüfung verzichten zu können
      3. Digits[0] = new RomanDigit() { Digit = 'M', Value = 1000, IsHelpBase = false };
      4. Digits[1] = new RomanDigit() { Digit = 'D', Value = 500, IsHelpBase = true };
      5. Digits[2] = new RomanDigit() { Digit = 'C', Value = 100, IsHelpBase = false };
      6. Digits[3] = new RomanDigit() { Digit = 'L', Value = 50, IsHelpBase = true };
      7. Digits[4] = new RomanDigit() { Digit = 'X', Value = 10, IsHelpBase = false };
      8. Digits[5] = new RomanDigit() { Digit = 'V', Value = 5, IsHelpBase = true };
      9. Digits[6] = new RomanDigit() { Digit = 'I', Value = 1, IsHelpBase = false };
      10. }
      11. public override string ToString() {
      12. StringBuilder sb = new StringBuilder();
      13. int rest = this.Value;
      14. var ubound = Digits.Length - 2;
      15. for(int i = 0; i < ubound; i++) {
      16. var additor = Digits[i];
      17. var additorCount = Math.DivRem(rest, additor.Value, out rest); //ganzzahl-teilung mit Rest
      18. sb.Append(new string(additor.Digit, additorCount)); //den additor.Digit so oft anhängen, wie er in rest hineinpasste.
      19. //subtrakions-regel: falls die Subtraktion des nächst-kleineren 10er-vielfachen doch noch in rest hineinpasst, diesen subtraktor dem additor voranstellen
      20. var subtractor = Digits[(i | 1) + 1];
      21. if(rest >= additor.Value - subtractor.Value) {
      22. sb.Append(subtractor.Digit).Append(additor.Digit);
      23. rest -= additor.Value - subtractor.Value;
      24. }
      25. }
      26. return sb.ToString();
      27. }
      übrigens sollteste unbedingt .Equals überschreiben, um ein konsistentes Verhalten zum ==-Operator zu gewährleisten.
      Sagt mir jdfs. eine Compilerwarnung, und das ist eine der vielen Richtlinien, die ich sehr befürworte (auch wennich sonst gelegentlich gegen unreflektiertes Übernehmen von Richtlinien polemisiere ;)).
      Hi
      und IEquatable<RomanNumber> implementieren auch, sowie GetHashCode überschreiben. Equals(object) ruft Equals(RomanNumber) auf, welches dann entsprechend bspw. den Operator aufruft. Equals(object) erfordert auch eine null-Überprüfung, Equals(RomanNumber) nicht, da eine struct nicht nullable ist, ist ja klar (falls das nicht klar war):

      C#-Quellcode

      1. public override bool Equals(object obj)
      2. {
      3. return obj != null && obj is RomanNumber && Equals((RomanNumber)obj);
      4. }
      5. public bool Equals(RomanNumber other)
      6. {
      7. return this == other;
      8. }
      9. public override int GetHashCode()
      10. {
      11. return _value /*.GetHashCode()*/;
      12. }


      und IComparable nicht vergessen, wie ich es gerade gemacht habe.

      Gruß
      ~blaze~

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „~blaze~“ ()

      Type-Converter wär doch eigentlich noch eine Idee. Zum Typ selbst würd's ja in einer Library schon, auch wenn man dann vmtl. den TypeConverter eher auf die Property setzen würde und je nachdem, ob ein numerischer String angegeben wurde, als solchen oder solchen auswerten würde (könnte man doch eigentlich vom von Integer verwendeten TypeConverter ableiten, weiß aber grad nicht, wo der definiert wurde und ob öffentlich und bin zu faul, nachzuschauen).

      Gruß
      ~blaze~
      Wie wärs mit nem ValueConverter? Das könnte man dann gleich in Verbindung mit [WPF] MVVM: "Binding-Picking" im Xaml-Editor verwenden.
      @ErfinderDesRades


      Opensource Audio-Bibliothek auf github: KLICK, im Showroom oder auf NuGet.