Fortgeschrittene Formatierung und Parsing von Strings


  • Administrator

    Hallo zusammen,

    Kennt ihr gute Quellen über die Formatierung und Parsing von Strings in .Net? Es geht mir wirklich um fortgeschrittene Themen, wie IFormattable , IFormatProvider und ICustomFormatter . In der MSDN gibt es zwar ein paar gute Seiten, wie z.B:
    http://msdn.microsoft.com/en-us/library/26etazsy.aspx

    Aber irgendwie steig ich noch nicht ganz durch. Ich sehe verschiedene Probleme:
    1. Wie muss ein eigener IFormatProvider aussehen, damit er korrekt funktioniert? Wenn man String.Format verwendet, kann man nur einen IFormatProvider angeben, wie weiss dann die Funktion, wann sie welches Format anwenden soll? Wie kann ich pro Typ unterschiedliche Formatierungen mitgeben?
    2. Wie kann ich sinnvoll die Formatierung der Primitiven steuern. Ich möchte zum Beispiel standardmässig CultureInfo.InvariantCulture verwenden, aber ohne bei Thread.CurrentCulture reinzupfuschen. Muss ich meine Funktionen für primitive Typen überladen oder kann ich das mit einer machen, welche einen bestimmten Typ erwartet? IFormattable ? Object ? Wie sieht das Ganze aus, wenn es vom String wieder in den primitiven Typ geht?
    3. Gibt es überhaupt eine vorbereitete Möglichkeit Strings zu parsen? Ingendwelche Interfaces oder sowas?

    Vielen Dank im Voraus.

    Grüssli



  • Hi,

    Genauer habe ich mich damit noch nicht befasst, daher kann ich dir nur zum Teil antworten.

    Dravere schrieb:

    Aber irgendwie steig ich noch nicht ganz durch. Ich sehe verschiedene Probleme:
    1. Wie muss ein eigener IFormatProvider aussehen, damit er korrekt funktioniert? Wenn man String.Format verwendet, kann man nur einen IFormatProvider angeben, wie weiss dann die Funktion, wann sie welches Format anwenden soll? Wie kann ich pro Typ unterschiedliche Formatierungen mitgeben?

    Ich glaub das ist nicht vorgesehen das die Parameter bei string.Format mit unterschiedlichen IFormatProvider ausgestattet werden. Du kannst aber bei den Typen eventuell das ToString() verwenden und dort den Formatter angeben.

    Dravere schrieb:

    2. Wie kann ich sinnvoll die Formatierung der Primitiven steuern. Ich möchte zum Beispiel standardmässig CultureInfo.InvariantCulture verwenden, aber ohne bei Thread.CurrentCulture reinzupfuschen. Muss ich meine Funktionen für primitive Typen überladen oder kann ich das mit einer machen, welche einen bestimmten Typ erwartet? IFormattable ? Object ? Wie sieht das Ganze aus, wenn es vom String wieder in den primitiven Typ geht?

    Du brauchst in CurrentCulture nichts ändern. CultureInfo ist auch ein IFormatProvider (Siehe http://msdn.microsoft.com/de-de/library/system.globalization.cultureinfo(v=vs.80).aspx), also kannst du sehr einfach steuern in welcher Culture es Formatiert werden soll.
    Z.B.:
    string->double

    double value = double.Parse("1.2", new CultureInfo("en-US"));
    

    double->string

    var d = 1.5;
    string value = d.ToString(new CultureInfo("en-US"));
    

    Dravere schrieb:

    3. Gibt es überhaupt eine vorbereitete Möglichkeit Strings zu parsen? Ingendwelche Interfaces oder sowas?

    Ich finde sind die "TryParse" Methoden am besten.
    Es gibt auch noch die "Converter" Klasse, diese basiert (unter anderem) darauf das die Typen IConvertibe implementiert haben, ich bevorzuge aber wie gesagt die [Try]Parse.

    //Dazu:
    Siehe auch: http://www.c-plusplus.net/forum/291901


  • Administrator

    David W schrieb:

    Ich glaub das ist nicht vorgesehen das die Parameter bei string.Format mit unterschiedlichen IFormatProvider ausgestattet werden. Du kannst aber bei den Typen eventuell das ToString() verwenden und dort den Formatter angeben.

    David W schrieb:

    Du brauchst in CurrentCulture nichts ändern. CultureInfo ist auch ein IFormatProvider (Siehe http://msdn.microsoft.com/de-de/library/system.globalization.cultureinfo(v=vs.80).aspx), also kannst du sehr einfach steuern in welcher Culture es Formatiert werden soll.
    Z.B.:
    string->double

    double value = double.Parse("1.2", new CultureInfo("en-US"));
    

    double->string

    var d = 1.5;
    string value = d.ToString(new CultureInfo("en-US"));
    

    ToString(IFormatProvider provider) kommt leider aus keinem Interface, sondern ist für jeden Typ explizit vorhanden. Bei z.B. System.Enum ist dieses sogar als obsolete gekennzeichnet. Es gibt nur ein ToString(string format, IFormatProvider provider) in IFormattable . Soweit ich es verstanden habe, kann man dann für format null übergeben.

    Wenn nun aber jemand IFormattable implementiert, wie müsste dieser nun genau vorgehen. Das ist mir irgendwie noch nicht so ganz klar. Und kann ich mit IFormattable alle abfangen?

    David W schrieb:

    Ich finde sind die "TryParse" Methoden am besten.
    Es gibt auch noch die "Converter" Klasse, diese basiert (unter anderem) darauf das die Typen IConvertibe implementiert haben, ich bevorzuge aber wie gesagt die [Try]Parse.

    Die Parse und TryParse sind aber statische Methoden und gehören daher zu keinem Interface. IConvertible ist nicht CLS-compliant. Ich suche ein gutes Pendant zu IFormattable und co.

    Ich hätte zum Beispiel am liebsten etwas gehabt, was ein DateTime automatisch als Unix Timestamp ausgibt und entsprechend wieder einliest, ohne dabei das Datum immer zuerst umrechnen zu müssen, bzw. dass dies irgendwo weggekapselt in der Formattierung passiert. Für die Ausgabe könnte ich es womöglich noch hinbekommen, aber beim Einlesen sehe ich schwarz. Wobei ich auch bei der Ausgabe Probleme sehe. Wenn ich ein IFormattable Object habe, was für einen IFormatProvider muss ich mitgeben, damit bei einem DateTime die richtige Unix Timestamp Formatierung angewendet wird, wenn es allerdings ein anderer Typ ist, dass dann trotzdem die korrekte Formatierung drankommt (also z.B. bei Int32 CultureInfo.InvariantCulture ).

    Nur damit es klar ist, ich weiss schon, wie man ein Unix Timestamp ausgeben und einlesen kann. Mir geht es wirklich nur darum, dass ich dies gerne wegkapseln möchte. Und ich hatte gehofft, dass es da schon grundsätzliche Schnittstellen gäbe.

    Grüssli



  • Dravere schrieb:

    ToString(IFormatProvider provider) kommt leider aus keinem Interface, sondern ist für jeden Typ explizit vorhanden.

    Was ich logisch finde, da jeder Typ anders geschrieben wird, wie soll man ein int auch mit einem TimeSpan vergleichen.

    Dravere schrieb:

    Bei z.B. System.Enum ist dieses sogar als obsolete gekennzeichnet. Es gibt nur ein ToString(string format, IFormatProvider provider) in IFormattable . Soweit ich es verstanden habe, kann man dann für format null übergeben.

    Ruf beim Enum einfach ohne FormatProvider auf, der Rückgabewert unterscheidet sich nicht bei den verschiedenen Cultures. Daher ist der IFormatProvider schlichtweg unnötig.

    Dravere schrieb:

    Wenn nun aber jemand IFormattable implementiert, wie müsste dieser nun genau vorgehen. Das ist mir irgendwie noch nicht so ganz klar. Und kann ich mit IFormattable alle abfangen?
    ...
    Ich hätte zum Beispiel am liebsten etwas gehabt, was ein DateTime automatisch als Unix Timestamp ausgibt und entsprechend wieder einliest, ohne dabei das Datum immer zuerst umrechnen zu müssen, bzw. dass dies irgendwo weggekapselt in der Formattierung passiert. Für die Ausgabe könnte ich es womöglich noch hinbekommen, aber beim Einlesen sehe ich schwarz. Wobei ich auch bei der Ausgabe Probleme sehe. Wenn ich ein IFormattable Object habe, was für einen IFormatProvider muss ich mitgeben, damit bei einem DateTime die richtige Unix Timestamp Formatierung angewendet wird, wenn es allerdings ein anderer Typ ist, dass dann trotzdem die korrekte Formatierung drankommt (also z.B. bei Int32 CultureInfo.InvariantCulture ).

    Du müsstest Problemlos ein "UnixTimeStampFormatter" schreiben können, auf dieser Seite findest du ein vollständiges Beispiel: http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx
    Aber das betrifft nur die Richtung DateTime -> String. Für den weg zurück wüsste ich gerade nichts. Ne Idee wäre lediglich das du die "UnixTimeStampFormatter" Klasse noch um ein "Parse", "TryParse", "ParseExact" sowie "TryParseExact" versehen kannst.

    Was wäre deine Wunschlösung? Die Parse und TryParse sind zwar explizit für jeden Typen, aber das ist doch in Ordnung, wüsste nicht das man da etwas generischeres braucht da man an der Stelle wo man es anwendet eh den genauen Typen kennen muss.


  • Administrator

    David W schrieb:

    Dravere schrieb:

    ToString(IFormatProvider provider) kommt leider aus keinem Interface, sondern ist für jeden Typ explizit vorhanden.

    Was ich logisch finde, da jeder Typ anders geschrieben wird, wie soll man ein int auch mit einem TimeSpan vergleichen.

    Es geht ja nicht ums Vergleichen, sondern eine gemeinsame Schnittstelle für das Formatieren und das ist mit dieser Funktion nicht gegeben.

    David W schrieb:

    Dravere schrieb:

    Bei z.B. System.Enum ist dieses sogar als obsolete gekennzeichnet. Es gibt nur ein ToString(string format, IFormatProvider provider) in IFormattable . Soweit ich es verstanden habe, kann man dann für format null übergeben.

    Ruf beim Enum einfach ohne FormatProvider auf, der Rückgabewert unterscheidet sich nicht bei den verschiedenen Cultures. Daher ist der IFormatProvider schlichtweg unnötig.

    Das ist aber nicht mein Ziel ... damit muss ich Überladungen schreiben für Enum-Werte.

    David W schrieb:

    Du müsstest Problemlos ein "UnixTimeStampFormatter" schreiben können, auf dieser Seite findest du ein vollständiges Beispiel: http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx
    Aber das betrifft nur die Richtung DateTime -> String.

    Wie das für einen einzelnen Typ geht, weiss ich schon. Aber was ist bei sowas:

    var text = String.Format(new UnixTimeStampFormatter(), "{0}: {1}", DateTime.Today, 33.4);
    

    Wie kann ich hier dafür sorgen, dass die 33.4 mit der CultureInfo.InvarantCultur formatiert wird, ohne dass ich Thread.CurrentCulture anfasse?

    David W schrieb:

    Was wäre deine Wunschlösung? Die Parse und TryParse sind zwar explizit für jeden Typen, aber das ist doch in Ordnung, wüsste nicht das man da etwas generischeres braucht da man an der Stelle wo man es anwendet eh den genauen Typen kennen muss.

    Ich möchte sowas anbieten:

    TValue GetValue<TValue>(string key, Parser<TValue> parser)
    {
      var text = dict[key];
      return parser.Parse(text, CultureInfo.InvarianCulture);
    }
    
    void SetValue<TValue>(string key, TValue value)
    {
      dict[key] = // ... ja wie? Wie sichere ich, dass hier InvariantCultur verwendet wird?
    }
    

    Und hatte gehofft, dass es bereits etwas von der Standardbibliothek gibt, was ich ausbauen oder wiederverwenden kann, damit ich mich gut ins Framework einpasse.
    Wobei ich bei SetValue eben auch noch gerne sowas anbieten möchte:

    void SetValue(string key, string format, params object[] objs)
    {
      dict[key] = String.Format(/* ja was? */, format, objs);
    }
    

    Wobei eben immer InvariantCulture verwendet wird, aber z.B. bei einem DateTime das Unix Timestamp Format. Und wenn möglich, möchte ich noch für ein paar weitere eigene Typen eine spezielle Formatierung anbieten. Und das Ganze dann eben auch in die andere Richtung.

    Grüssli



  • [quote="David W"]

    Dravere schrieb:

    David W schrieb:

    Dravere schrieb:

    Bei z.B. System.Enum ist dieses sogar als obsolete gekennzeichnet. Es gibt nur ein ToString(string format, IFormatProvider provider) in IFormattable . Soweit ich es verstanden habe, kann man dann für format null übergeben.

    Ruf beim Enum einfach ohne FormatProvider auf, der Rückgabewert unterscheidet sich nicht bei den verschiedenen Cultures. Daher ist der IFormatProvider schlichtweg unnötig.

    Das ist aber nicht mein Ziel ... damit muss ich Überladungen schreiben für Enum-Werte.

    Erstmal irrelevant da du derzeit ja noch nicht einmal eine gemeinsame Schnittstelle hast 😃

    David W schrieb:

    Wie das für einen einzelnen Typ geht, weiss ich schon. Aber was ist bei sowas:

    var text = String.Format(new UnixTimeStampFormatter(), "{0}: {1}", DateTime.Today, 33.4);
    

    Wie kann ich hier dafür sorgen, dass die 33.4 mit der CultureInfo.InvarantCultur formatiert wird, ohne dass ich Thread.CurrentCulture anfasse?

    Nicht ganz schön aber funktioniert:

    var value = 33.4;
    var text = string.Format(new CultureInfo(1033), "{0}: {1}", DateTime.Today, value.ToString(CultureInfo.InvariantCulture));
    

    Was du eventuell machen könntest wäre sowas wie ich es auch in meiner DW.Configuration gemacht habe, und zwar registrierst du beim Applikations start für jeden Typen einen "Marshaller" der es entsprechend Konvertiert.
    Es gibt ein generischen IMarshaller, der nimmt ein XElement und gibt Object zurück (da fällt mir ein das ich es eventuell noch generisch machen kann ^^)

    Z.B.:

    public class DateTimeMarshaller : IMarshaller
    {
    	public object Extract(XElement settingNode)
    	{
    		var text = settingNode.Value;
    		if (!string.IsNullOrWhiteSpace(text))
    		{
    			DateTime converted = DateTime.Now;
    			if (DateTime.TryParse(text, new CultureInfo(1033), DateTimeStyles.None, out converted))
    				return converted;
    		}
    		return null;
    	}
    
    	public void Append(XElement settingNode, object value)
    	{
    		if (value is DateTime)
    		{
    			var converted = (DateTime)value;
    			settingNode.Value = converted.ToString(new CultureInfo(1033));
    		}
    		else
    			settingNode.Value = value.ToString();
    	}
    }
    

    (https://github.com/DavidWDev/DW.Configurations/blob/master/DW.Configurations/Marshallers/DateTimeMarshaller.cs)

    Im "SettingsManager" sagt man dann welcher Key mit welchen Marshaller gelesen und geschrieben werden soll:

    public static class SettingsManager
    {
    	public static void Load()
    	{
    		var reader = new SettingsReader(GetFilePath(), new DefaultsContainer());
    		RegisterMarshaller(reader);
    		reader.Read();
    	}
    
    	public static void Save()
    	{
    		var writer = new SettingsWriter(GetFilePath());
    		RegisterMarshaller(writer);
    		writer.Write();
    	}
    
    	private static void RegisterMarshaller(SettingsHandler writer)
    	{
    		writer.SetKeysMarshaller(new BooleanMarshaller(), "Boolean"); // Hier kann man eine Liste von Keys angeben die dann mit dem Marshaller Konvertiert werden
    		writer.SetKeysMarshaller(new DateTimeMarshaller(), "DateTime");
    		writer.SetKeysMarshaller(new DoubleMarshaller(), "Double");
    		writer.SetKeysMarshaller(new IntegerMarshaller(), "Integer");
    		writer.SetKeysMarshaller(new LongMarshaller(), "Long");
    		writer.SetKeysMarshaller(new ShortMarshaller(), "Short");
    		writer.SetKeysMarshaller(new StringMarshaller(), "String");
    	}
    
    	private static string GetFilePath()
    	{
    		return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Settings.cfg");
    	}
    }
    

    (https://github.com/DavidWDev/DW.Configurations/blob/master/DW.Configurations.Demo/SettingsManager.cs)

    Ich habe die Verwendung ganz einfach:

    SettingsManager.Load(); // Settings laden
    var value = Settings.Get<int>("Integer"); // Wert lesen, "Integer" ist der Key
    

    (https://github.com/DavidWDev/DW.Configurations/blob/master/DW.Configurations.Demo/App.xaml.cs)

    So ähnlich könntest du es auch Lösen.


  • Administrator

    So ähnlich habe ich es bis jetzt gelöst 😉
    Meine "Marshaller" verbinden sich allerdings gleich mit dem Typ. Also ein:
    RegisterMarshaller(Type type, IMarshaller marshaller);

    Ich habe mich allerdings bisher eben gefragt, ob ich damit nicht Features aus der .Net Bibliothek umschiffe. Ob man es nicht direkt über IFormattable oder dergleichen lösen könnte.

    Grüssli



  • Interessant wie man doch zu ähnlichen Lösungen kommt ^^
    Hast du den "IMarshaller" über Generics gelöst?


  • Administrator

    David W schrieb:

    Hast du den "IMarshaller" über Generics gelöst?

    Wie sollte man dies denn über Generics hinbekommen? Man muss den ja irgendwie in ein Dictionary quetschen. Ich habe mir aber eine Hilfsklasse erstellt in der Hierarchie. Also etwas in diese Richtung:

    interface ITextConverter
    {
      object ToObject(string text);
      string ToText(object obj);
    }
    
    abstract class AbstractTextConverter<TObject>
      : ITextConverter
    {
      protected abstract TObject ToTypedObject(string text);
      protected abstract string ToText(TObject obj);
    
      public object ToObject(string text)
      {
        return ToTypedObject(text);
      }
    
      public string ToText(object obj)
      {
        if(obj is TObject)
        {
          return ToText((TObject)obj);
        }
    
        throw new InvalidOperationException("Type not supported!");
      }
    }
    

    Grüssli



  • Ich dachte an irgendwie sowas:

    interface IConverter<T>
    {
      T FromText(string text);
      string ToText<T>(T obj);
    }
    
    public class IntegerConverter : IConverter<int>
    {
        public int FromText(string text) { ... }
        public string ToText(int obj) { ... }
    }
    

    Hab es grad hier im Forum getippt aber nicht getestet ob es überhaupt baut.
    Aber jetzt wo ich es seh merk ich auch das man es so nicht in ein Dictionary bekommt - vermutlich hatte ich es aus dem selben Grund auch damals nicht gemacht ^^


Anmelden zum Antworten