Format TimeSpan in DataGridView column - c#

I've seen these questions but both involve methods that aren't available in the CellStyle Format value. I only want to show the hours and minutes portion (16:05); not the seconds as well (16:05:13). I tried forcing the seconds value to zero but still got something like 16:05:00. Short of using a kludge like providing a string or a DateTime (and only showing the hour/minutes part) is there any way I can get the formatting to do what I want.

I just discovered this myself. Unfortunately, the solution is pretty involved. The good news is that it works.
Firstly, you need an ICustomFormatter implementation that deals with TimeSpan values. The .NET framework does not include such a type out-of-the-box; I am guessing this is because Microsoft didn't want to have to deal with the ambiguity involved in formatting a TimeSpan (e.g., does "hh" mean total hours or only the hour component?) and the ensuing onslaught of support issues that would arise when these ambiguities confused developers.
That's OK -- just implement your own. Below is a sample class I wrote that uses basically the same custom format strings as DateTime (those that were applicable, anyway)*:
class TimeSpanFormatter : IFormatProvider, ICustomFormatter
{
private Regex _formatParser;
public TimeSpanFormatter()
{
_formatParser = new Regex("d{1,2}|h{1,2}|m{1,2}|s{1,2}|f{1,7}", RegexOptions.Compiled);
}
#region IFormatProvider Members
public object GetFormat(Type formatType)
{
if (typeof(ICustomFormatter).Equals(formatType))
{
return this;
}
return null;
}
#endregion
#region ICustomFormatter Members
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (arg is TimeSpan)
{
var timeSpan = (TimeSpan)arg;
return _formatParser.Replace(format, GetMatchEvaluator(timeSpan));
}
else
{
var formattable = arg as IFormattable;
if (formattable != null)
{
return formattable.ToString(format, formatProvider);
}
return arg != null ? arg.ToString() : string.Empty;
}
}
#endregion
private MatchEvaluator GetMatchEvaluator(TimeSpan timeSpan)
{
return m => EvaluateMatch(m, timeSpan);
}
private string EvaluateMatch(Match match, TimeSpan timeSpan)
{
switch (match.Value)
{
case "dd":
return timeSpan.Days.ToString("00");
case "d":
return timeSpan.Days.ToString("0");
case "hh":
return timeSpan.Hours.ToString("00");
case "h":
return timeSpan.Hours.ToString("0");
case "mm":
return timeSpan.Minutes.ToString("00");
case "m":
return timeSpan.Minutes.ToString("0");
case "ss":
return timeSpan.Seconds.ToString("00");
case "s":
return timeSpan.Seconds.ToString("0");
case "fffffff":
return (timeSpan.Milliseconds * 10000).ToString("0000000");
case "ffffff":
return (timeSpan.Milliseconds * 1000).ToString("000000");
case "fffff":
return (timeSpan.Milliseconds * 100).ToString("00000");
case "ffff":
return (timeSpan.Milliseconds * 10).ToString("0000");
case "fff":
return (timeSpan.Milliseconds).ToString("000");
case "ff":
return (timeSpan.Milliseconds / 10).ToString("00");
case "f":
return (timeSpan.Milliseconds / 100).ToString("0");
default:
return match.Value;
}
}
}
We're not finished yet. With this type in place, you are equipped to assign a custom formatter to the column in your DataGridView that you want to use for displaying your TimeSpan values.
Let's say that column is called "Time"; then you would do this:
DataGridViewColumn timeColumn = dataGridView.Columns["Time"];
timeColumn.DefaultCellStyle.FormatProvider = new TimeSpanFormatter();
timeColumn.DefaultCellStyle.Format = "hh:mm";
So now you're set up, right?
Well, for some odd reason, you're still not 100% of the way there. Why custom formatting can't kick in at this point, I honestly couldn't tell you. But we're almost done. The one final step is to handle the CellFormatting event to get this new functionality we've written to actually take effect:
private void dataGridView_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
var formatter = e.CellStyle.FormatProvider as ICustomFormatter;
if (formatter != null)
{
e.Value = formatter.Format(e.CellStyle.Format, e.Value, e.CellStyle.FormatProvider);
e.FormattingApplied = true;
}
}
At last, we're finished. Setting the DefaultCellStyle.Format property of the DataGridViewColumn you want formatted according to your custom rules should now work as expected.
*So, "h"/"hh" for hours, "m"/"mm" for minutes. etc.

It is possible to achieve the effect same by just using the CellFormatting event.
private void dataGridView_CellFormatting(object sender,
DataGridViewCellFormattingEventArgs e)
{
if (e.Value != null && e.Value != DBNull.Value)
e.Value = ((TimeSpan)e.Value).Hours.ToString("00") + ":" +
((TimeSpan)e.Value).Minutes.ToString("00");
}
This obviously is not as comprehensive a solution, but quite quick.

Try the following code
dataGridView1.Columns["columnName"].DefaultCellStyle.Format = "hh\\:mm";

I don't know how to set the format of the cell to show only hours and minutes. I'd suggest you set the format of the cell to string and format the value like this:
String.Format("{0:D2}:{1:D2}",
DateTime.Now.TimeOfDay.Hours, DateTime.Now.TimeOfDay.Minutes);

Use format string "hh\\:mm".
e.g
YourGrid.Column[index].DefaultCellStyle.Format = "hh\\:mm"

Try another approach. Just add to your class binding to the datagridview properties like for instance LastPacketAtTimeDelayAsStr.
Let's say you have some class that has it...
public DateTime? LastPacketAtTime { get; set; }
public TimeSpan? LastPacketAtTimeDelay
{
get
{
if (LastPacketAtTime.HasValue)
{
var ts = DateTime.Now - LastPacketAtTime.Value;
return ts;
}
return null;
}
}
public string LastPacketAtTimeDelayAsStr
{
get
{
if (LastPacketAtTimeDelay.HasValue)
{
var hours = LastPacketAtTimeDelay.Value.Hours.ToString("00");
var minutes = LastPacketAtTimeDelay.Value.Minutes.ToString("00");
var seconds = LastPacketAtTimeDelay.Value.Seconds.ToString("00");
return $"{LastPacketAtTimeDelay.Value.Days} days {hours}:{minutes}:{seconds}";
}
return null;
}
}
And after that just bind the LastPacketAtTimeDelayAsStr to the DataGridView column you need which has String datatype.
And that's it!

Related

Too many methods very similar

I have many methods which are very similar as shown in the code below:
public static void ReadFromKeyboard(string label, out int retVal)
{
try
{
Console.Write(label);
retVal = int.Parse(Console.ReadLine());
}
catch (Exception)
{
Console.WriteLine("Please insert int value.");
ReadFromKeyboard(label, out retVal);
}
}
public static void ReadFromKeyboard(string label, out float retVal)
{
try
{
Console.Write(label);
retVal = float.Parse(Console.ReadLine());
}
catch (Exception)
{
Console.WriteLine("Please insert float value.");
ReadFromKeyboard(label, out retVal);
}
}
public static void ReadFromKeyboard(string label, out double retVal)
{
try
{
Console.Write(label);
retVal = double.Parse(Console.ReadLine());
}
catch (Exception)
{
Console.WriteLine("Please insert double value.");
ReadFromKeyboard(label, out retVal);
}
}
By the other hand, I don't know which method I will call. I'll discorver it only at runtime.
Is there any way I could rewrite these many methods into a single method named something like "ReadFromKeyboard" which returns either an int, a float or a double depending on the type which is passed to it as a parameter?
Thank you!
As other answers have shown, you can eliminate the duplicated code by a variety of techniques, all of which are horrible and you should not do them.
In particular, do not attempt to use generics to solve this "problem". Generics are for situations where the code is generic. That is why they are called generics! That is, the code operates the same on every possible type. Your example is the opposite of generic code; you have different rules for a small number of types, and the way to handle that situation is to do exactly what you have already done: implement one method per different rule.
I say "problem" in quotes because you do not actually have a problem to solve here, so stop trying to solve it. Writing half a dozen similar short methods is not a major burden on authors or maintainers.
Now, that said, your code is also not as good as it could be and you should rewrite it. The correct way to write your code is:
public static int ReadInteger(string label)
{
while(true)
{
int value;
Console.Write(label);
string read = Console.ReadLine();
bool success = int.TryParse(read, out value);
if (success)
return value;
Console.WriteLine("Please type an integer value.");
}
}
The problems with your original implementation are:
Do not use exception handling as mainline control flow. Do not catch an exception if the exception can be avoided. That's what TryParse is for.
Do not use recursion as unbounded looping. If you want an unbounded loop, that's what while(true) is for. Remember, C# is not tail recursive by default!
Do not use out parameters without need. The method logically returns an integer, so actually return an integer. Rename it so that you do not get collisions with other read methods. There is no compelling benefit to making the caller write Read<int> over ReadInteger, and many compelling benefits to avoiding the out param.
I've tried to implement the code according to Eric Lippert recipes. The code below
does not use exception handling as mainline control flow
does not use recursion at all
does not use output parameters without need
.
private static void Main(string[] args)
{
int intValue = ReadFromKeyboardInt32("enter int");
float floatValue = ReadFromKeyboardSingle("enter float");
double doubleValue = ReadFromKeyboardDouble("enter double");
Console.WriteLine($"{intValue}, {floatValue}, {doubleValue}");
}
public static Double ReadFromKeyboardDouble(string label) =>
ReadFromKeyboard(label, (text) => (Double.TryParse(text, out var value), value));
public static Int32 ReadFromKeyboardInt32(string label) =>
ReadFromKeyboard(label, (text) => (Int32.TryParse(text, out var value), value));
public static Single ReadFromKeyboardSingle(string label) =>
ReadFromKeyboard(label, (text) => (Single.TryParse(text, out var value), value));
public static T ReadFromKeyboard<T>(string label, Func<string, (bool, T)> tryParse)
{
for (; ; )
{
Console.Write($"{label}: ");
var result = tryParse(Console.ReadLine());
if (result.Item1)
{
return result.Item2;
}
Console.WriteLine($"Please enter valid {typeof(T).Name} value");
}
}
Instead of listing all the possible types (which you might not know beforehand), it is possible to use the System.Convert class, specially the Convert.ChangeType() method. As a proof of concept you can use a method like this:
public static void ReadFromKeyboard<T>(string label, out T result) {
Type targetType = typeof(T);
Console.Write($"{label}: ");
string input = Console.ReadLine();
object convertedValue = Convert.ChangeType(input, targetType);
result = (T)convertedValue;
}
You can use this method like this:
public static void Main(string[] args) {
ReadFromKeyboard("enter a double", out double d);
ReadFromKeyboard("enter an int", out int i);
Console.WriteLine($"double: {d}");
Console.WriteLine($"int: {i}");
}
This way it is possible to use any type you want (assuming it is supported by the Convert class). Obviously you can add exception handling and a do-while loop in the ReadFromKeyboard method if you like.
If you want to rely on overload resolution for the runtime to decide which method to call, then you must have a separate method for each type you will support. That's how it works.
On the other hand, if you can allow the user to supply at least a little type information, we can improve things a bit with generics by removing try/catch and using a real return statement. You'd call it like this:
var myNumber = ReadFromKeyboard<double>("Enter a double: ");
And the code would look like this:
public static T ReadFromKeyboard<T>(string label, int maxRetries = int.MaxValue)
{
while (maxRetries >= 0)
{
Console.Write(label);
if (typeof(T) == typeof(int))
{
int result;
if (int.TryParse(Console.ReadLine(), out result)) return (T)(object)result;
}
if (typeof(T) == typeof(float))
{
float result;
if (float.TryParse(Console.ReadLine(), out result)) return (T)(object)result;
}
else if (typeof(T) == typeof(double))
{
double result;
if (double.TryParse(Console.ReadLine(), out result)) return (T)(object)result;
}
else if (typeof(T) == typeof(decimal))
{
decimal result;
if (decimal.TryParse(Console.ReadLine(), out result)) return (T)(object)result;
}
else
throw new InvalidOperationException("Unsupported type");
maxRetries--;
}
throw new InvalidOperationException("Too many bad inputs");
}
But you have to do some really janky casting and type checking to make it work. There is still a potential this can throw an exception, which it seems like you want to avoid, but if your user sits there for more than 2 billion attempts, I doubt they'll be very surprised.

Convert System.DayOfWeek into Microsoft.Office.Interop.Outlook.OlDaysOfWeek

Is there a possibility to directly convert a value of System.DayOfWeek into a value of Microsoft.Office.Interop.Outlook.OlDaysOfWeek. Something like:
var day = DayOfWeek.Friday;
OlDaysOfWeek days = ConvertToDaysOfWeek(day);
To do this without using string manipulation, here's an option:
public OlDaysOfWeek ConvertToDaysOfWeek(DayOfWeek day)
{
return (OlDaysOfWeek)Math.Pow(2, (int)day);
}
You can try this:
var olDay = (OlDaysOfWeek) Enum.Parse(typeof(OlDaysOfWeek), $"ol{systemDay}");
A simple switch statement would probably be most performant:
public OlDaysOfWeek ConvertToDaysOfWeek(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Monday: return OlDaysOfWeek.olMonday;
case DayOfWeek.Tuesday: return OlDaysOfWeek.olTuesday;
case DayOfWeek.Wednesday: return OlDaysOfWeek.olWednesday;
case DayOfWeek.Thursday: return OlDaysOfWeek.olThursday;
case DayOfWeek.Friday: return OlDaysOfWeek.olFriday;
case DayOfWeek.Saturday: return OlDaysOfWeek.olSaturday;
case DayOfWeek.Sunday: return OlDaysOfWeek.olSunday;
default: throw new ArgumentOutOfRangeException("What day is this?", "day");
}
}
Alternatively, you could probably parse the value and return the mapped value based on the enum value name.
public OlDaysOfWeek ConvertToDaysOfWeek(DayOfWeek day)
{
return (OlDaysOfWeek) Enum.Parse(typeof(OlDaysOfWeek), "ol" + day.ToString());
}
The OlDaysOfWeek enum utilises a power-of-2 sequence, which is typically used when combining values as bitwise flags. DaysOfWeek has a simple linear sequence reflected as 0-6 - this is why you can't compare using the backing int value
Finally I found the following solution. Thanks for the hints.
public static OlDaysOfWeek AsDaysOfWeek(this DayOfWeek dayOfWeek)
{
return (OlDaysOfWeek)(1 << (int)dayOfWeek);
}
To be used like this:
var day = DayOfWeek.Friday;
OlDaysOfWeek days = day.AsDaysOfWeek();
Additionally these are the unit tests in NUnit for the method:
[TestCase(DayOfWeek.Monday, OlDaysOfWeek.olMonday)]
[TestCase(DayOfWeek.Tuesday, OlDaysOfWeek.olTuesday)]
[TestCase(DayOfWeek.Wednesday, OlDaysOfWeek.olWednesday)]
[TestCase(DayOfWeek.Thursday, OlDaysOfWeek.olThursday)]
[TestCase(DayOfWeek.Friday, OlDaysOfWeek.olFriday)]
[TestCase(DayOfWeek.Saturday, OlDaysOfWeek.olSaturday)]
[TestCase(DayOfWeek.Sunday, OlDaysOfWeek.olSunday)]
public void AsDaysOfWeek(DayOfWeek dayOfWeek, OlDaysOfWeek expectedResult)
{
var result = dayOfWeek.AsDaysOfWeek();
Assert.That(result, Is.EqualTo(expectedResult));
}

Extending the custom formatting capabilities of built-in types

I have some rather awkward formatting requirements for decimal values. In a nutshell: display to two decimal places with a trailing space unless the third decimal is a 5, in which case display to three decimal places.
This formatting needs to be fairly flexible, too. Specifically, the trailing space will not always be desired, and a "½" may be preferred when the third decimal is a "5".
Examples:
1.13 would be displayed as "01.13 " with a space or "01.13" without it
1.315 would be displayed as "01.315" or "01.31½"
I need to use this logic consistently across otherwise unrelated pieces of UI. I have temporarily written it as a WPF value converter, but this is just for demonstration:
public sealed class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is decimal))
{
return DependencyProperty.UnsetValue;
}
var decimalValue = (decimal)value;
var formattedDecimalValue = decimalValue.ToString("#0.000", CultureInfo.InvariantCulture);
var lastFormattedChar = formattedDecimalValue[formattedDecimalValue.Length - 1];
switch (lastFormattedChar)
{
case '0':
return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + " ";
case '5':
return formattedDecimalValue.Substring(0, formattedDecimalValue.Length - 1) + "½";
default:
return formattedDecimalValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I am now trying to extract this into a more fundamental building block I can use throughout my UI layer. My initial thought was a custom format provider which I could then use from a Binding:
<TextBlock Text="{Binding Value, FormatString=WHATEVER}"/>
The idea is that format string could be something like "#0.005" which indicates to only show the third decimal place if it's a 5, or "#0.00F" which attempts to represent the third decimal as a fraction. However, I was unable to find a means of using a specific format provider from a binding, which seems like a major limitation to me, but maybe I'm missing something...?
After more experimentation and investigation, I came to the conclusion that my only option is to define my own type:
public struct Price : IFormattable
This type would encapsulate the extra formatting capabilities I require. However, now I have another conundrum: in my ToString implementation, how can I leverage the existing formatting capabilities of decimal.ToString(string, IFormatProvider) without interfering with my own? It seems like this would be pretty darn messy, and it's causing me to lean towards a more limited solution of just defining "G" (two or three decimal places, no trailing space) and "S" (same as "G", but with trailing space if necessary) formats for my Price structure.
Can anyone tell me whether there's a way for me to do this kind of custom formatting capability without too much hassle?
See http://msdn.microsoft.com/en-us/library/system.iformatprovider.aspx for more details.
// "01.13 " or "01.13". Standard formatting applied: $123.45
// "01.315" or "01.31½". Standard formatting applied: $123.45
public class Test
{
void Main()
{
decimal number1 = 1.13M;
decimal number2 = 1.315M;
string output1 = String.Format(new CustomNumberFormat(),
"\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
number1, 123.45);
Console.WriteLine(output1);
string output2 = String.Format(new CustomNumberFormat(),
"\"{0:G}\" or \"{0:S}\". Standard formatting applied: {1:C2}",
number2, 123.45);
Console.WriteLine(output2);
}
}
public class CustomNumberFormat : System.IFormatProvider, System.ICustomFormatter
{
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
public string Format(string fmt, object arg, System.IFormatProvider formatProvider)
{
// Provide default formatting if arg is not a decimal.
if (arg.GetType() != typeof(decimal))
try
{
return HandleOtherFormats(fmt, arg);
}
catch (FormatException e)
{
throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
}
// Provide default formatting for unsupported format strings.
string ufmt = fmt.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
if (!(ufmt == "G" || ufmt == "S"))
try
{
return HandleOtherFormats(fmt, arg);
}
catch (FormatException e)
{
throw new FormatException(String.Format("The format of '{0}' is invalid.", fmt), e);
}
// Convert argument to a string.
string result = ((decimal)arg).ToString("0#.000");
if (ufmt == "G")
{
var lastFormattedChar = result[result.Length - 1];
switch (lastFormattedChar)
{
case '0':
result = result.Substring(0, result.Length - 1) + " ";
break;
}
return result;
}
else if (ufmt == "S")
{
var lastFormattedChar = result[result.Length - 1];
switch (lastFormattedChar)
{
case '0':
result = result.Substring(0, result.Length - 1);
break;
case '5':
result = result.Substring(0, result.Length - 1) + "½";
break;
}
return result;
}
else
{
return result;
}
}
private string HandleOtherFormats(string format, object arg)
{
if (arg is System.IFormattable)
return ((System.IFormattable)arg).ToString(format, System.Globalization.CultureInfo.CurrentCulture);
else if (arg != null)
return arg.ToString();
else
return String.Empty;
}
}
Try passing in your format provider as the parameter argument in your IValueConverter.Convert implementation:
<TextBlock Text="{Binding Value, Mode=OneWay, Converter={StaticResource PriceConverter}, ConverterParameter=#0.00F"/>
Then, inside your converter:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string formatString = parameter as string;
if(formatString != null)
{
// Your code here
}
else
{
// Whatever you want to do here
}
}

Decimal Rounding Problem

the following is my property where if we enter 45 then it appends 45.00 but then again it results in 45 because the value is converted from string. So what is the easiest way i can achieve this goal. Where if they enter 45 then it would result 45.00 in the value field;
public decimal Length
{
get { if (this is Detail)
return ((this as Detail).Length.ToString() == string.Empty)
? 0 : (this as Detail).Length; else return 0; }
set
{
if (this is Detail)
{
string val = string.Empty;
if (!value.ToString().Contains("."))
{
val = string.Format("{0}{1}", value.ToString(), ".00");
value =Math.Round(Convert.ToDecimal(val), 2);
}
else
value = Math.Round(value, 2);
(this as Detail).Length = (value.ToString().Trim() ==
string.Empty) ? 0 : value;
}
}
}
val = string.Format("{0:0.00}", value);
This has nothing to do with the property setter. You need to specify the string format in your GUI to round the numbers.
Also if (this is PersonalDetail) is a massive design flaw. Override the Length property in the PersonalDetail class instead. (not sure what the intent is with this property)
The problem is that 45m and 45.00m are the same thing, and since this is a decimal, it will always display "45" instead of "45.00" unless you use a string formatter every time you try to output it.
You could always make another property that does output what you want, such as:
public decimal Length { get; set; }
public string FormattedLength
{
get
{
return String.Format("{0:0.00}", this.Length);
}
}
On a side note I don't like this, but I believe it gets you more or less what you are looking for.

How to implement VARIANT in Protobuf

As part of my protobuf protocol I require the ability to send data of a dynamic type, a little bit like VARIANT. Roughly I require the data to be an integer, string, boolean or "other" where "other" (e.g. DateTime) is serialized as a string. I need to be able to use these as a single field and in lists in a number of different locations in the protocol.
How can this best be implemented while keeping message size minimal and performance optimal?
I'm using protobuf-net with C#.
EDIT:
I've posted a proposed answer below which uses what I think is the minimum of memory required.
EDIT2:
Created a github.com project at http://github.com/pvginkel/ProtoVariant with a complete implementation.
Jon's multiple optionals covers the simplest setup, especially if you need cross-platform support. On the .NET side (to ensure you don't serialize unnecessary values), simply return null from any property that isn't a match, for example:
public object Value { get;set;}
[ProtoMember(1)]
public int? ValueInt32 {
get { return (Value is int) ? (int)Value : (int?)null; }
set { Value = value; }
}
[ProtoMember(2)]
public string ValueString {
get { return (Value is string) ? (string)Value : null; }
set { Value = value; }
}
// etc
You can also do the same using the bool ShouldSerialize*() pattern if you don't like the nulls.
Wrap that up in a class and you should be fine to use that at either the field level or list level. You mention optimal performance; the only additional thing I can suggest there is to perhaps consider treating as a "group" rather than "submessage", as this is easier to encode (and just as easy to decode, as long as you expect the data). To do that, use the Grouped data-format, via [ProtoMember], i.e.
[ProtoMember(12, DataFormat = DataFormat.Group)]
public MyVariant Foo {get;set;}
However, the difference here can be minimal - but it avoids some back-tracking in the output stream to fix the lengths. Either way, in terms of overheads a "submessage" will take at least 2 bytes; "at least one" for the field-header (perhaps taking more if the 12 is actually 1234567) - and "at least one" for the length, which gets bigger for longer messages. A group takes 2 x the field-header, so if you use low field-numbers this will be 2 bytes regardless of the length of the encapsulated data (it could be 5MB of binary).
A separate trick, useful for more complex scenarios but not as interoperable, is generic inheritance, i.e. an abstract base class that has ConcreteType<int>, ConcreteType<string> etc listed as subtypes - this, however, takes an extra 2 bytes (typically), so is not as frugal.
Taking another step further away from the core spec, if you genuinely can't tell what types you need to support, and don't need interoperability - there is some support for including (optimized) type information in the data; see the DynamicType option on ProtoMember - this takes more space than the other two options.
You could have a message like this:
message Variant {
optional string string_value = 1;
optional int32 int32_value = 2;
optional int64 int64_value = 3;
optional string other_value = 4;
// etc
}
Then write a helper class - and possibly extension methods - to ensure that you only ever set one field in the variant.
You could optionally include a separate enum value to specify which field is set (to make it more like a tagged union) but the ability to check the optional fields just means the data is already there. It depends on whether you want the speed of finding the right field (in which case add the discriminator) or the space efficiency of only including the data itself (in which case don't add the discriminator).
That's a general Protocol Buffer approach. There may be something more protobuf-net specific, of course.
Asking questions always helps me think. I found a way to get the number of bytes used for transfer to a bare minimum.
What I've done here is make use of optional properties. Say I want to send an int32. When the value isn't zero, I can just check a property on the message for whether it has a value. Otherwise, I set a type to INT32_ZERO. This way I can correctly store and reconstruct the value. The example below has this implementation for a number of types.
The .proto file:
message Variant {
optional VariantType type = 1 [default = AUTO];
optional int32 value_int32 = 2;
optional int64 value_int64 = 3;
optional float value_float = 4;
optional double value_double = 5;
optional string value_string = 6;
optional bytes value_bytes = 7;
optional string value_decimal = 8;
optional string value_datetime = 9;
}
enum VariantType {
AUTO = 0;
BOOL_FALSE = 1;
BOOL_TRUE = 2;
INT32_ZERO = 3;
INT64_ZERO = 4;
FLOAT_ZERO = 5;
DOUBLE_ZERO = 6;
NULL = 7;
}
And accompanying partial .cs file:
using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
namespace ConsoleApplication6
{
partial class Variant
{
public static Variant Create(object value)
{
var result = new Variant();
if (value == null)
result.Type = VariantType.NULL;
else if (value is string)
result.ValueString = (string)value;
else if (value is byte[])
result.ValueBytes = (byte[])value;
else if (value is bool)
result.Type = (bool)value ? VariantType.BOOLTRUE : VariantType.BOOLFALSE;
else if (value is float)
{
if ((float)value == 0f)
result.Type = VariantType.FLOATZERO;
else
result.ValueFloat = (float)value;
}
else if (value is double)
{
if ((double)value == 0d)
result.Type = VariantType.DOUBLEZERO;
else
result.ValueDouble = (double)value;
}
else if (value is decimal)
result.ValueDecimal = ((decimal)value).ToString("r", CultureInfo.InvariantCulture);
else if (value is DateTime)
result.ValueDatetime = ((DateTime)value).ToString("o", CultureInfo.InvariantCulture);
else
throw new ArgumentException(String.Format("Cannot store data type {0} in Variant", value.GetType().FullName), "value");
return result;
}
public object Value
{
get
{
switch (Type)
{
case VariantType.BOOLFALSE:
return false;
case VariantType.BOOLTRUE:
return true;
case VariantType.NULL:
return null;
case VariantType.DOUBLEZERO:
return 0d;
case VariantType.FLOATZERO:
return 0f;
case VariantType.INT32ZERO:
return 0;
case VariantType.INT64ZERO:
return (long)0;
default:
if (ValueInt32 != 0)
return ValueInt32;
if (ValueInt64 != 0)
return ValueInt64;
if (ValueFloat != 0f)
return ValueFloat;
if (ValueDouble != 0d)
return ValueDouble;
if (ValueString != null)
return ValueString;
if (ValueBytes != null)
return ValueBytes;
if (ValueDecimal != null)
return Decimal.Parse(ValueDecimal, CultureInfo.InvariantCulture);
if (ValueDatetime != null)
return DateTime.Parse(ValueDatetime, CultureInfo.InvariantCulture);
return null;
}
}
}
}
}
EDIT:
Further comments from #Marc Gravell have improved the implementation significantly. See the Git repository for a complete implementation of this concept.
Actually protobuf doesn't support any kind of VARIANT types.
You can try to play around using Unions, see more details here
The main idea is to define message wrapper with all existing message types as optional field, and by using union just specify which type of this concrete message it is.
See example by following the link above.
I use ProtoInclude with an abstract base type and subclasses to get the type and single value statically set. Here's the start of what that could look like for Variant:
[ProtoContract]
[ProtoInclude(1, typeof(Integer))]
[ProtoInclude(2, typeof(String))]
public abstract class Variant
{
[ProtoContract]
public sealed class Integer
{
[ProtoMember(1)]
public int Value;
}
[ProtoContract]
public sealed class String
{
[ProtoMember(1)]
public string Value;
}
}
Usage:
var foo = new Variant.String { Value = "Bar" };
var baz = new Variant.Integer { Value = 10 };
This answer gives takes a bit more space as it encodes the length of the ProtoInclude'd class instance (e.g. 1 byte for int and under < 125 byte strings). I am willing to live with this for the benefit of controlling the type statically.

Categories

Resources