diff --git a/Program.cs b/Program.cs index 9b1ed09..0196634 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Net.Http; using System.Reflection; @@ -18,11 +20,13 @@ namespace ZaoBot static JsonDocument config; static HttpClient http = new HttpClient(); + const string CMD_WEATHER = "Погода"; const string CMD_LEFT_GATE = "Открой левые ворота"; const string CMD_RIGHT_GATE = "Открой правые ворота"; static void Main(string[] args) { + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; config = LoadConfig(); var token = config.RootElement.GetProperty("token").GetString(); botClient = new TelegramBotClient(token); @@ -48,18 +52,32 @@ namespace ZaoBot } Console.WriteLine($"Received a text message in chat {e.Message.Chat.Id}."); + var response = ""; if (e.Message.Text == CMD_LEFT_GATE) { - await OpenLeftGate(); + response = await OpenLeftGate(); } - if (e.Message.Text == CMD_RIGHT_GATE) + else if (e.Message.Text == CMD_RIGHT_GATE) { - await OpenRightGate(); + response = await OpenRightGate(); } + else if (e.Message.Text == CMD_WEATHER) + { + response = await ShowWeather(); + } + else if (e.Message.Text == "") + { + response = "Превед!"; + } + else response = "Я не понял :-("; + + if (e.Message.Text != "") response += "\nЧто дальше?"; + else response = "\nЧего изволите?"; var replyKeyboardMarkup = new ReplyKeyboardMarkup( new KeyboardButton[][] { + new KeyboardButton[] { CMD_WEATHER }, new KeyboardButton[] { CMD_LEFT_GATE, CMD_RIGHT_GATE }, }, resizeKeyboard: true @@ -67,31 +85,75 @@ namespace ZaoBot await botClient.SendTextMessageAsync( chatId: e.Message.Chat, - text: "Чего изволите?", + text: response, replyMarkup: replyKeyboardMarkup ); } } - private static async Task OpenRightGate() + private static async Task ShowWeather() + { + try + { + string response = ""; + foreach (var element in config.RootElement.GetProperty("items_weather").EnumerateObject()) + { + var url = config.RootElement.GetProperty("openhab_url").GetString(); + url += element.Name; + var result = await http.GetAsync(url); + var content = await result.Content.ReadAsStringAsync(); + response += element.Value + ": " + ParseContent(content) + "\n"; + } + return response; + } + catch (Exception e) + { + return "Fail: " + e.Message; + } + } + + private static string ParseContent(string content) + { + var doc = JsonDocument.Parse(content); + float value; + if (!float.TryParse(doc.RootElement.GetProperty("state").GetString(), out value)) + { + return "null"; + } + try + { + var stateDescription = doc.RootElement.GetProperty("stateDescription"); + var pattern = stateDescription.GetProperty("pattern"); + return printf.Printf.sprintf(pattern.GetString(), value); + } + catch (KeyNotFoundException) + { + return value.ToString(); + } + } + + private static async Task OpenRightGate() { var url = config.RootElement.GetProperty("openhab_url").GetString(); url += config.RootElement.GetProperty("item_right_gate").GetString(); - await Post(url, "ON"); + return await Post(url, "ON"); } - private static async Task Post(string url, string v) + private static async Task Post(string url, string v) { var content = new ByteArrayContent(Encoding.UTF8.GetBytes(v)); content.Headers.Add("Content-Type", "text/plain"); - await http.PostAsync(url, content); + var result = await http.PostAsync(url, content); + if (result.IsSuccessStatusCode) return "OK"; + else return "Fail. " + result.ReasonPhrase; + } - private static async Task OpenLeftGate() + private static async Task OpenLeftGate() { var url = config.RootElement.GetProperty("openhab_url").GetString(); url += config.RootElement.GetProperty("item_left_gate").GetString(); - await Post(url, "ON"); + return await Post(url, "ON"); } private static bool VerifyUser(long id, string username) diff --git a/lib/FormatObject.cs b/lib/FormatObject.cs new file mode 100644 index 0000000..fb0b630 --- /dev/null +++ b/lib/FormatObject.cs @@ -0,0 +1,535 @@ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace printf { + /// + /// Applies string formatting to objects using a "printf" format string. + /// Can be extended to use custom formats. + /// + public class FormatObject { + /// + /// The default precision for floating point numbers + /// + public const int DefaultPrecision = 6; + + /// + /// Represents a formatting part of the format string (i.e. %1.3d) + /// All fields are interpreted as specified in the C language specification + /// + public class FormatStringPart { + /// + /// Minimum width + /// + public int width = 0; + /// + /// Precision + /// + public int? precision; + /// + /// Length (h or l) + /// + public char length; + /// + /// Specifier (s for strings, d for integers etc.) + /// + public char specifier; + + /// + /// Left aligns the result instead of the default right align + /// + public bool LeftAlign = false; + /// + /// Displays + cahracter for positive numbers (by default it is omitted) + /// + public bool ForcePlus = false; + /// + /// Displays ' ' (space) character for positive numbers (by default it is omitted) + /// + public bool BlankIfPlus = false; + /// + /// Has different meanings for format specifiers + /// + public bool HashMark = false; + /// + /// If LeftAlign = false, pads with the '0' character instead of ' '. + /// If LeftAlign = true, this flag is ignored. + /// + public bool PadWithZero = false; + } + + //List of actual arguments + List args = new List(); + + //Number of required arguments + int argCount = 0; + + //Format string parts + FormatStringPart[] parts; + string[] staticParts; + + string finalString; + + enum ParseMode { + Static, Format + } + enum FormatStep { + Flags, Width, PrecisionStart, Precision, Length, Specifier + } + + /// + /// Creates a new formatter with the default formatters (used by the printf functions). + /// + /// + public FormatObject(string format): this(format, true) {} + + /// + /// Creates a new formatter with or without the default formatters (used by the printf functions). + /// + /// The format string + /// Load the default formatters or not + public FormatObject(string format, bool useDefaultFormatters) { + + //State machine to parse the format string + //Surprisingly, the regexp version is not really shorter: + //Parsing the string is easier, but extracting the results is longer + + List parts = new List(); + List staticParts = new List(); + StringBuilder staticPart = new StringBuilder(); + + //Static mode: not in a % block, Format mode: in a % block + ParseMode mode = ParseMode.Static; + FormatStringPart part = null; + FormatStep step = FormatStep.Flags; + StringBuilder tempBuffer = null; + + for (int i = 0; i < format.Length; ++i) { + char c = format[i]; + + if (mode == ParseMode.Static) { + //If + if (c == '%') { + mode = ParseMode.Format; + step = FormatStep.Flags; + } + else { + staticPart.Append(c); + } + } + else { + //For the terminology (flags, specifiers etc.) look up any printf documentation. + switch (step) { + case FormatStep.Flags: + if (c == '%') { + staticPart.Append('%'); + mode = ParseMode.Static; + continue; + } + if (part == null) part = new FormatStringPart(); + if (c == '-') { + part.LeftAlign = true; + } + else if (c == '+') { + part.ForcePlus = true; + } + else if (c == ' ') { + part.BlankIfPlus = true; + } + else if (c == '#') { + part.HashMark = true; + } + else if (c == '0') { + part.PadWithZero = true; + } + else { + step = FormatStep.Width; + goto case FormatStep.Width; + } + break; + case FormatStep.Width: + if (tempBuffer != null) { + if (c >= '0' && c <= '9') { + tempBuffer.Append(c); + } + else { + part.width = int.Parse(tempBuffer.ToString()); + tempBuffer = null; + step = FormatStep.PrecisionStart; + goto case FormatStep.PrecisionStart; + } + } + else if (c == '*') { + part.width = -1; + argCount++; + } + else if (c >= '0' && c <= '9') { + tempBuffer = new StringBuilder(); + tempBuffer.Append(c); + } + else { + step = FormatStep.PrecisionStart; + goto case FormatStep.PrecisionStart; + } + break; + case FormatStep.PrecisionStart: + if (c != '.') { + step = FormatStep.Length; + goto case FormatStep.Length; + } + else { + step = FormatStep.Precision; + } + break; + case FormatStep.Precision: + if (tempBuffer != null) { + if (c >= '0' && c <= '9') { + tempBuffer.Append(c); + } + else { + part.precision = int.Parse(tempBuffer.ToString()); + tempBuffer = null; + step = FormatStep.Length; + goto case FormatStep.Length; + } + } + else if (c == '*') { + part.precision = -1; + argCount++; + } + else if (c >= '0' && c <= '9') { + tempBuffer = new StringBuilder(); + tempBuffer.Append(c); + } + else { + part.precision = 0; + step = FormatStep.Length; + goto case FormatStep.Length; + } + break; + case FormatStep.Length: + if ("hlL".IndexOf(c) != -1) { + part.length = c; + step = FormatStep.Specifier; + } + else { + step = FormatStep.Specifier; + goto case FormatStep.Specifier; + } + break; + case FormatStep.Specifier: + argCount++; + part.specifier = c; + + staticParts.Add(staticPart.ToString()); + staticPart = new StringBuilder(); + parts.Add(part); + part = null; + + mode = ParseMode.Static; + break; + } + } + } + if (mode == ParseMode.Format) { + throw new ArgumentException("Invalid format string", "format"); + } + staticParts.Add(staticPart.ToString()); + + this.parts = parts.ToArray(); + this.staticParts = staticParts.ToArray(); + + if (useDefaultFormatters) LoadDefaultFormatters(); + } + + /// + /// Set the objects to format. + /// If they are already set, the function will overwrite them. + /// This is useful if you want to use the same format string to + /// format many objects. + /// + /// The list of objects + public void SetArgs(params object[] objs) { + for (int i = 0; i < objs.Length; ++i) { + args.Add(objs[i]); + } + this.finalString = null; + } + + /// + /// Formats the object according to the FormatStringPart argument. + /// If you create a custom formatter, you should ignore the width property, + /// as padding is done independently for all format types. + /// + /// It contains the flags, specifier etc. + /// The object to format + public delegate FormatResult Formatter(FormatStringPart part, object arg); + + Dictionary formatters = new Dictionary(); + + /// + /// Adds a custom formatter. + /// + /// The character in the format string (e.g. 's' for %s) + /// The formatter function + public void AddFormatter(char specifier, Formatter formatter) { + formatters.Add(specifier, formatter); + } + + private void DoFormat() { + try { + if (argCount > args.Count) { + throw new InvalidOperationException(string.Format( + "Expected argument count: {0}, provided: {1}", argCount, args.Count)); + } + StringBuilder final = new StringBuilder(); + for (int i = 0, j = 0; i < parts.Length; ++i, ++j) { + final.Append(staticParts[i]); + var f = parts[j]; + if (f.width == -1) { + f.width = (int)args[j]; + j++; + } + if (f.precision == -1) { + f.width = (int)args[j]; + j++; + } + FormatResult afterSpecifiers = formatters[f.specifier].Invoke(f, args[j]); + int pad = f.width - afterSpecifiers.Result.Length - afterSpecifiers.Sign.Length; + if (pad > 0 && !f.LeftAlign) { + if (f.PadWithZero) { + final.Append(afterSpecifiers.Sign); + final.Append(new string('0', pad)); + } + else { + final.Append(new string(' ', pad)); + final.Append(afterSpecifiers.Sign); + } + } + else { + final.Append(afterSpecifiers.Sign); + } + final.Append(afterSpecifiers.Result); + if (pad > 0 && f.LeftAlign) { + final.Append(new string(' ', pad)); + } + } + final.Append(staticParts[staticParts.Length-1]); + finalString = final.ToString(); + } + catch (KeyNotFoundException ex) { + throw new InvalidOperationException("Invalid specifier in format string", ex); + } + } + + /// + /// Returns the formatted string. + /// + /// The formatted string + public override string ToString() { + if (this.finalString == null) DoFormat(); + return finalString; + } + + /// + /// The results of the Formatter type functions. + /// + public struct FormatResult { + /// + /// The resulting strings sign, if any. + /// + public string Sign {get; set;} + /// + /// The resulting string, without the sign if any. + /// + public string Result {get; set;} + + /// + /// Converts to string into a FormatResult object. + /// The string is assigned to the Result property of the created FormatString, + /// the Sign property will be "" (empty string). + /// + /// The string to convert + /// A FormatResult object + public static implicit operator FormatResult(string s) { + return new FormatResult { + Sign = "", Result = s + }; + } + } + + private string IntPrecision(string str, FormatStringPart part) { + if (str.Equals("0") && part.precision == 0) return ""; + else if (str.Length < part.precision) { + return new string('0', (part.precision ?? 1) - str.Length) + str; + } + else { + return str; + } + } + + private void LoadDefaultFormatters() { + var format = new System.Globalization.NumberFormatInfo(); + format.NumberDecimalSeparator = "."; + + Formatter charFormatter = (part, arg) => { + char c = (char)arg; + return c.ToString(); + }; + AddFormatter('c', charFormatter); + Formatter intFormatter = (part, arg) => { + long i = Convert.ToInt64(arg); + string sign = ""; + if (i >= 0) { + if (part.ForcePlus) sign = "+"; + else if (part.BlankIfPlus) sign = " "; + } + else { + i = -i; + sign = "-"; + } + return new FormatResult { Result = IntPrecision(i.ToString(), part), Sign = sign }; + }; + AddFormatter('i', intFormatter); + AddFormatter('d', intFormatter); + Formatter uintFormatter = (part, arg) => { + ulong u = Convert.ToUInt64(arg); + string sign = ""; + if (u >= 0) { + if (part.ForcePlus) sign = "+"; + else if (part.BlankIfPlus) sign = " "; + } + return new FormatResult { Result = IntPrecision(u.ToString(), part) , Sign = sign }; + }; + AddFormatter('u', uintFormatter); + Formatter sciFormatter = (part, arg) => { + double d = Convert.ToDouble(arg); + string sign = ""; + if (d >= 0 || double.IsNaN(d)) { + if (part.ForcePlus) sign = "+"; + else if (part.BlankIfPlus) sign = " "; + } + else { + d = -d; + sign = "-"; + } + string retStr = d.ToString(string.Concat("0.", + new string('0', part.precision ?? DefaultPrecision), + part.specifier.ToString(), + "+000"), + format); + //# flag: place decimal point even if not needed + if (part.HashMark && !retStr.Contains(format.NumberDecimalSeparator)) { + retStr = string.Join(format.NumberDecimalSeparator + part.specifier, + retStr.Split(part.specifier)); + } + return new FormatResult { + Result = retStr, + Sign = sign + }; + }; + AddFormatter('e', sciFormatter); + AddFormatter('E', sciFormatter); + Formatter floatFormatter = (part, arg) => { + double d = Convert.ToDouble(arg); + string sign = ""; + if (d >= 0) { + if (part.ForcePlus) sign = "+"; + else if (part.BlankIfPlus) sign = " "; + } + else { + d = -d; + sign = "-"; + } + format.NumberDecimalDigits = part.precision ?? DefaultPrecision; + string retStr = d.ToString("f", format); + //# flag: place decimal point even if not needed + if (part.HashMark && !retStr.Contains(format.NumberDecimalSeparator)) { + retStr += format.NumberDecimalSeparator; + } + return new FormatResult { Result = retStr, Sign = sign }; + }; + AddFormatter('f', floatFormatter); + + Formatter gFormatter = (part, arg) => { + double d = Convert.ToDouble(arg); + string sign = ""; + if (d >= 0) { + if (part.ForcePlus) sign = "+"; + else if (part.BlankIfPlus) sign = " "; + } + else { + d = -d; + sign = "-"; + } + format.NumberDecimalDigits = part.precision ?? DefaultPrecision; + /*string formatString = part.specifier.ToString() + part.precision.ToString() + + (part.HashMark ? ":0." + new string('#', part.precision ?? DefaultPrecision) : ""); + string retStr = string.Format(format, formatString, d);*/ + //Could not make it work + string retStr = d.ToString(part.specifier.ToString() + part.precision.ToString(), format); + return new FormatResult { Result = retStr, Sign = sign }; + }; + AddFormatter('g', gFormatter); + AddFormatter('G', gFormatter); + + Formatter octFormatter = (part, arg) => { + long l = Convert.ToInt64(arg); + string retStr = IntPrecision(Convert.ToString(l, 8), part); + //# flag: put a 0 before tha number + if (part.HashMark) { + return "0" + retStr; + } + else { + return retStr; + } + }; + AddFormatter('o', octFormatter); + Formatter hexFormatter = (part, arg) => { + string retStr; + if (part.length == 'h') { + short s = Convert.ToInt16(arg); + retStr = string.Format( + part.specifier == 'x' ? "{0:x}" : "{0:X}", + s); + } + else if (part.length == 'l') { + long l = Convert.ToInt64(arg); + retStr = string.Format( + part.specifier == 'x' ? "{0:x}" : "{0:X}", + l); + } + else { + int i = Convert.ToInt32(arg); + retStr = string.Format( + part.specifier == 'x' ? "{0:x}" : "{0:X}", + i); + } + //# flag: put a 0x before tha number + return new FormatResult { + Result = IntPrecision(retStr, part), + Sign = part.HashMark ? "0" + part.specifier : "" + }; + }; + AddFormatter('x', hexFormatter); + AddFormatter('X', hexFormatter); + Formatter strFormatter = (part, arg) => { + if (part.precision != null) { + return arg.ToString().Remove(part.precision.Value); + } + else { + return arg.ToString(); + } + }; + AddFormatter('s', strFormatter); + Formatter ptrFormatter = (part, arg) => { + return arg.GetHashCode().ToString(); + }; + AddFormatter('p', ptrFormatter); + AddFormatter('n', (part, arg) => { + throw new NotSupportedException("Getting the number of characters is not supported"); + }); + } + } +} diff --git a/lib/FormatTest.cs b/lib/FormatTest.cs new file mode 100644 index 0000000..b4cdfae --- /dev/null +++ b/lib/FormatTest.cs @@ -0,0 +1,241 @@ + +#if TEST + +using System; +using NUnit.Framework; + +namespace printf { + [TestFixture] + public class FormatTest { + string decSep = System.Globalization.NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator; + + [Test] + public void TestTypes() { + //Test the basic data types + Assert.AreEqual("b", + Printf.sprintf("%c", 'b')); + Assert.AreEqual("121", + Printf.sprintf("%d", 121)); + Assert.AreEqual("-34", + Printf.sprintf("%i", -34)); + Assert.AreEqual("3.123400e+002", + Printf.sprintf("%e", 3.1234e02)); + Assert.AreEqual("3.123400E+002", + Printf.sprintf("%E", 3.1234e02)); + Assert.AreEqual("3.123000", + Printf.sprintf("%f", 3.123)); + Assert.AreEqual("610", + Printf.sprintf("%o", Convert.ToInt32("0610", 8))); + Assert.AreEqual("asdfjkl", + Printf.sprintf("%s", "asdfjkl")); + Assert.AreEqual("4567", + Printf.sprintf("%u", 4567)); + Assert.AreEqual("deadf00d", + Printf.sprintf("%x", unchecked((int)0xdeadf00d))); + Assert.AreEqual("DEADF00D", + Printf.sprintf("%X", unchecked((int)0xdeadf00d))); + Assert.IsNotEmpty(Printf.sprintf("%p", new object())); + + Assert.Throws(delegate() { + Printf.sprintf("%n", new IntPtr()); + }); + + //Invalid format string + Assert.Throws(delegate() { + Printf.sprintf("%z", new IntPtr()); + }); + + Assert.AreEqual("%", + Printf.sprintf("%%")); + } + + [Test] + public void TestStaticText() { + //Static text + Assert.AreEqual("aaaa", + Printf.sprintf("aaaa")); + //Param at the end + Assert.AreEqual("Hello World", + Printf.sprintf("Hello %s", "World")); + //Param at the beginning + Assert.AreEqual("Hello World!", + Printf.sprintf("%s World!", "Hello")); + //Param in the middle + Assert.AreEqual("a 1 b", + Printf.sprintf("a %d b", 1)); + //Multiple params + Assert.AreEqual("Hello World!", + Printf.sprintf("%s %s!", "Hello", "World")); + } + + [Test] + public void TestArgumentCount() { + //More arguments should not cause errors + Assert.DoesNotThrow(delegate() { + Printf.sprintf("%d %d", 1, 2, 3); + }); + //Less arguments should raise error + Assert.Throws(delegate() { + Printf.sprintf("%d %d", 1); + }); + //Also check for the extra arguments required for * + Assert.Throws(delegate() { + Printf.sprintf("%*d", 1); + }); + Assert.Throws(delegate() { + Printf.sprintf("%.*d", 1); + }); + Assert.Throws(delegate() { + Printf.sprintf("%*.*d", 1, 2); + }); + Assert.DoesNotThrow(delegate() { + Printf.sprintf("%*.*d", 1, 2, 3); + }); + } + + [Test] + public void TestPadding() { + //Simple padding + Assert.AreEqual(" 33", + Printf.sprintf("%5d", 33)); + //More padding than required + Assert.AreEqual("123456", + Printf.sprintf("%4d", 123456)); + //Left-align + Assert.AreEqual("888 ", + Printf.sprintf("%-5d", 888)); + //Pad with zeros + Assert.AreEqual("0043, 00zz", + Printf.sprintf("%04d, %04s", 43, "zz")); + //Left align always pads with spaces + Assert.AreEqual("123 ", + Printf.sprintf("%-05d", 123)); + //Width specified on the argument list + Assert.AreEqual(" 11", + Printf.sprintf("%*d", 6, 11)); + //Sign with padding + Assert.AreEqual(" -1 -0001 0001", + Printf.sprintf("%5d %05d % 05d", -1, -1, 1)); + } + + [Test] + public void TestSign() { + //Sign of number + Assert.AreEqual("-5", + Printf.sprintf("%d", -5)); + //Positive signs + Assert.AreEqual("3 +4", + Printf.sprintf("%d %+d", 3, 4)); + //Sign is not printed for o x and X + Assert.AreEqual("610 ffffffff FFFFFFFF", + Printf.sprintf("%+o %+x %+X", Convert.ToInt32("0610", 8), -1, -1)); + //Insert space if there is no sign + Assert.AreEqual("-11 22", + Printf.sprintf("% d % d", -11, 22)); + } + + [Test] + public void TestPrecision() { + //INTEGERS: minimum number of digits + Assert.AreEqual("123", + Printf.sprintf("%.2d", 123)); + Assert.AreEqual("ff", + Printf.sprintf("%.1x", 0xff)); + //Padding with 0 + Assert.AreEqual("0044", + Printf.sprintf("%.4d", 44)); + Assert.AreEqual("000FF", + Printf.sprintf("%.5X", 0xff)); + Assert.AreEqual("003", + Printf.sprintf("%.3u", 3)); + Assert.AreEqual("0010", + Printf.sprintf("%.4o", 8)); + //Sign is not included: + Assert.AreEqual("-01", + Printf.sprintf("%.2d", -1)); + //Mixing: + Assert.AreEqual(" 001", + Printf.sprintf("%5.3d", 1)); + + //Zero: + Assert.AreEqual("00", + Printf.sprintf("%.2d", 0)); + Assert.AreEqual("", + Printf.sprintf("%.0d", 0)); + Assert.AreEqual("", + Printf.sprintf("%.d", 0)); + + //Floating point: + Assert.AreEqual("0.1235", + Printf.sprintf("%.4f", 0.123456)); + Assert.AreEqual("0", + Printf.sprintf("%.0f", 0.123456)); + //Mixing: + Assert.AreEqual(" 0.123", + Printf.sprintf("%6.3f", 0.123)); + Assert.AreEqual("0.123", + Printf.sprintf("%4.3f", 0.123)); + + //Scientific: + Assert.AreEqual("1.2346e-001", + Printf.sprintf("%.4e", 0.123456)); + Assert.AreEqual("1e-001", + Printf.sprintf("%.0e", 0.123456)); + //Mixing: + Assert.AreEqual("1.235e-001", + Printf.sprintf("%4.3e", 0.123456)); + Assert.AreEqual(" 1.235e-001", + Printf.sprintf("%15.3e", 0.123456)); + + + //String: + Assert.AreEqual("asd", + Printf.sprintf("%.3s", "asdfjkl")); + Assert.AreEqual("", + Printf.sprintf("%.s", "asdfjkl")); + //Mixing: + Assert.AreEqual(" asd", + Printf.sprintf("%6.3s", "asdfjkl")); + } + + [Test] + public void TestHashMark() { + //Prefix for o, h and H + Assert.AreEqual("011 0xff 0XABC", + Printf.sprintf("%#o %#x %#X", 9, 0xff, 0xabc)); + + //Decimal point for f, e and E + Assert.AreEqual("1. 2.e+000 3.E+000", + Printf.sprintf("%#.0f %#.0e %#.0E", 1.1, 2.1, 3.1)); + Assert.AreEqual("1.1 2.1e+000 3.1E+000", + Printf.sprintf("%#.1f %#.1e %#.1E", 1.1, 2.1, 3.1)); + + //Mixing with 0 and padding + Assert.AreEqual("0x0ab", + Printf.sprintf("%#05x", 0xab)); + } + + [Test] + public void TestGSpecifier() { + Assert.AreEqual("0.0001", + Printf.sprintf("%g", 0.0001)); + Assert.AreEqual("1e-05", + Printf.sprintf("%g", 0.00001)); + Assert.AreEqual("1E-05", + Printf.sprintf("%G", 0.00001)); + + //Trailing zeros + Assert.AreEqual("0.01", + Printf.sprintf("%.3g", 0.01)); + Assert.AreEqual("0.010", + Printf.sprintf("%.3f", 0.01)); + //Assert.AreEqual("0.010", + // Printf.sprintf("%#.3g", 0.01)); + + //Precision = significant digits + Assert.AreEqual(" 10.1", + Printf.sprintf("%9.3g", 10.111111)); + } + } +} +#endif diff --git a/lib/Printf.cs b/lib/Printf.cs new file mode 100644 index 0000000..f5bc665 --- /dev/null +++ b/lib/Printf.cs @@ -0,0 +1,69 @@ + +using System; + +namespace printf { + /// + /// Contains the printf family of functions. + /// It supports the following formats: 'diufeExXoscp'; + /// The following flags: '-+ #0'; + /// Width and precision; + /// Length (h, l) for hexadecimal output (for other formats, integers are treated as Int64 or UInt64) + /// + /// Decimal separator is not localized, it is always a '.' character. + /// %g and %G does not remove trailing zeroes. + /// NaN and infinities are not guaranteed to have a fixed representation accross platforms. + /// + /// At the moment, it does NOT support %n (number of chars printed, through a prointer), + /// and the 1$..n$ position specifier. + /// + public static class Printf { + /// + /// Formats the arguments according to the format string. + /// Returns the result as a string. + /// + /// The format string + /// The objects to format + /// The formatted output + /// The format string is invalid or too few arguments provided. + /// Format string is null + public static string sprintf(string format, params object[] args) { + if (format == null) throw new ArgumentNullException("format"); + try { + FormatObject f = new FormatObject(format); + f.SetArgs(args); + return f.ToString(); + } + catch(InvalidOperationException ex) { + throw new ArgumentException("Error in format string or arguments, see inner exception for details", ex); + } + } + + /// + /// Formats the arguments according to the format string, + /// and writes the formatted string to the standard output. + /// + /// The format string + /// The objects to format + /// The format string is invalid or too few arguments provided. + /// Format string is null + public static void printf(string format, params object[] args) { + if (format == null) throw new ArgumentNullException("format"); + Console.Write(sprintf(format, args)); + } + + /// + /// Formats the arguments according to the format string, + /// and writes the formatted string to the specified TextWriter. + /// + /// The target TextWriter + /// The format string + /// The objects to format + /// The format string is invalid or too few arguments provided. + /// Format string or TextWriter is null + public static void fprintf(System.IO.TextWriter writer, string format, params object[] args) { + if (format == null) throw new ArgumentNullException("format"); + if (writer == null) throw new ArgumentNullException("writer"); + writer.Write(sprintf(format, args)); + } + } +}