├── .gitignore ├── .pubignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── sprintf.dart └── src │ ├── formatters │ ├── Formatter.dart │ ├── float_formatter.dart │ ├── int_formatter.dart │ └── string_formatter.dart │ └── sprintf_impl.dart ├── pubspec.yaml ├── test ├── sprintf_test.dart └── testing_data.dart └── tool └── gentests.py /.gitignore: -------------------------------------------------------------------------------- 1 | pubspec.lock 2 | .project 3 | packages 4 | .packages 5 | tools/test_data.dart 6 | *~ 7 | .pydevproject 8 | .settings 9 | .dart_tool 10 | .vscode 11 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | test/* 2 | tool/* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - stable 4 | - beta 5 | - dev 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v7.0.0 (2022-09-30) 2 | 3 | * properly format Infinity and NaN for %f and %G (#33) 4 | 5 | v6.0.2 (2022-08-10) 6 | 7 | * pub policy compliance 8 | 9 | v6.0.1 (2022-02-24) 10 | 11 | * Upgrade analyze package to dart:lint 12 | * code formatting 13 | 14 | v6.0.0 (2021-03-04) 15 | 16 | * Promote null-safety to stable 17 | 18 | v6.0.0-nullsafety (2020-12-10) 19 | 20 | * Add null-safety support 21 | 22 | v5.0.0 (2020-09-25) 23 | 24 | * Fix #22: position arguments didn't work. Major version bump because it's 25 | a different API. The following now works: 26 | ``` 27 | sprintf('|%2\$d %2\$d %1\$d|', [5, 10]); // '|10 10 5|' 28 | ``` 29 | 30 | 31 | v4.1.0 (2020-07-26) 32 | 33 | * Used dart padentic to fix up some code 34 | * changed SDK requirements to be >=2, <3 35 | 36 | v4.0.3 (2020-07-26) 37 | 38 | * Fixed formatting of "1.0" (#18); it was being truncated to "1" and then 39 | failing the regex. 40 | 41 | v4.0.2 (2019-02-19) 42 | 43 | * Fixed v4.0.1 44 | * Fixed rounding bug if 0 or 1 digits of rounding are requested (#15) 45 | 46 | v4.0.1 (2019-02-19) 47 | 48 | * **BROKEN** Fixed rounding bug if 0 or 1 digits of rounding are requested (#15) 49 | 50 | v4.0.0 (2018-07-02) 51 | 52 | * **Breaking change** (#13) 53 | As of dart 2, int types are going to be fixed width, and the width depends on 54 | implementation (dart2js vs dartvm). Dart started warning about constants that 55 | are greater than the max int of javascript. To fix that, this patch will limit 56 | int (when formatted as hex or octal) to the max limit of javascript (2**53 - 1). 57 | Any int outside of the +-(2**53 -1) range is not guaranteed to format correctly! 58 | 59 | v3.0.2 (2017-08-17) 60 | 61 | * Fixed bad publish 62 | 63 | v3.0.1 (2017-08-17) 64 | 65 | * Fixed weak typing which stopped dartdevc from working (#11 thanks @bergwerf) 66 | 67 | v3.0.0 (2017-04-20) 68 | 69 | * Fixed rounding of floats, again. Bumping major version because it's different from previous behaviour 70 | 71 | v2.0.0 (2017-03-23) 72 | 73 | * Fixed rounding of floats. Bumping major version, because previous behaviour was to always round down. 74 | 75 | v1.1.1 (2016-11-16) 76 | 77 | * Updated to use `test` library since `unittest` has been deprecated. 78 | 79 | v1.1.0 (2014-08-21) 80 | 81 | * **API change**: Any object with a valid `toString` method can be formatted with `'%s'` (#7) 82 | 83 | v1.0.9 (2013-12-15) 84 | 85 | * Round numbers when they're truncated. (#6) 86 | 87 | v1.0.8 (2013-10-08) 88 | 89 | * Remove trailing decimal point if there are no following digits (#6) 90 | 91 | v1.0.7 (2013-05-07) 92 | 93 | * Update for new dart version 94 | 95 | v1.0.6 (2013-04-16) 96 | 97 | * Update for new dart version 98 | 99 | v1.0.5 (2013-03-25) 100 | 101 | * Fixed dependencies example in README (#5) 102 | 103 | v1.0.4 (2013-03-21) 104 | 105 | * Update for new dart version 106 | 107 | * Update for new pubspec format 108 | 109 | v1.0.3 (2013-02-12) 110 | 111 | * Update for dart M2 112 | 113 | v1.0.2 (2013-01-21) 114 | 115 | * Pubspec and whitespace fixes 116 | 117 | v1.0.1 (2012-11-12) 118 | 119 | * Update for new dart version 120 | 121 | v1.0.0 (2012-10-08) 122 | 123 | First release with semver 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Richard Eames 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dart-sprintf 2 | 3 | Dart implementation of sprintf. 4 | 5 | [![Build Status](https://travis-ci.org/Naddiseo/dart-sprintf.svg?branch=master)](https://travis-ci.org/Naddiseo/dart-sprintf/) 6 | 7 | ## ChangeLog 8 | 9 | [ChangeLog.md](CHANGELOG.md) 10 | 11 | ## Getting Started 12 | 13 | Add the following to your **pubspec.yaml**: 14 | 15 | ``` 16 | dependencies: 17 | sprintf: "^7.0.0" 18 | ``` 19 | 20 | then run **pub install**. 21 | 22 | Next, import dart-sprintf: 23 | 24 | ``` 25 | import 'package:sprintf/sprintf.dart'; 26 | ``` 27 | 28 | ### Example 29 | 30 | ``` 31 | import 'package:sprintf/sprintf.dart'; 32 | 33 | void main() { 34 | print(sprintf("%04i", [-42])); 35 | print(sprintf("%s %s", ["Hello", "World"])); 36 | print(sprintf("%#04x", [10])); 37 | } 38 | ``` 39 | 40 | ``` 41 | -042 42 | Hello World 43 | 0x0a 44 | ``` 45 | 46 | ## Limitations 47 | 48 | - Negative numbers are wrapped as 64bit ints when formatted as hex or octal. 49 | 50 | Differences to C's printf 51 | 52 | - When using fixed point printing of numbers with large exponents, C introduces errors after 20 decimal places. Dart-printf will just print 0s. 53 | -------------------------------------------------------------------------------- /lib/sprintf.dart: -------------------------------------------------------------------------------- 1 | library sprintf; 2 | 3 | import 'dart:math'; 4 | 5 | part 'src/formatters/Formatter.dart'; 6 | part 'src/formatters/int_formatter.dart'; 7 | part 'src/formatters/float_formatter.dart'; 8 | part 'src/formatters/string_formatter.dart'; 9 | part 'src/sprintf_impl.dart'; 10 | 11 | //typedef SPrintF = String Function(String fmt, args); 12 | 13 | var sprintf = PrintFormat(); 14 | -------------------------------------------------------------------------------- /lib/src/formatters/Formatter.dart: -------------------------------------------------------------------------------- 1 | part of sprintf; 2 | 3 | abstract class Formatter { 4 | var fmt_type; 5 | var options; 6 | 7 | Formatter(this.fmt_type, this.options); 8 | 9 | static String get_padding(int count, String pad) { 10 | var padding_piece = pad; 11 | var padding = StringBuffer(); 12 | 13 | while (count > 0) { 14 | if ((count & 1) == 1) { 15 | padding.write(padding_piece); 16 | } 17 | count >>= 1; 18 | padding_piece = '${padding_piece}${padding_piece}'; 19 | } 20 | 21 | return padding.toString(); 22 | } 23 | 24 | String asString(); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/formatters/float_formatter.dart: -------------------------------------------------------------------------------- 1 | part of sprintf; 2 | 3 | class FloatFormatter extends Formatter { 4 | // ignore: todo 5 | // TODO: can't rely on '.' being the decimal separator 6 | static final _number_rx = RegExp(r'^[\-\+]?(\d+)\.(\d+)$'); 7 | static final _expo_rx = RegExp(r'^[\-\+]?(\d)\.(\d+)e([\-\+]?\d+)$'); 8 | static final _leading_zeroes_rx = RegExp(r'^(0*)[1-9]+'); 9 | 10 | double _arg; 11 | final List _digits = []; 12 | int _exponent = 0; 13 | int _decimal = 0; 14 | bool _is_negative = false; 15 | bool _has_init = false; 16 | String? _output; 17 | 18 | FloatFormatter(this._arg, var fmt_type, var options) 19 | : super(fmt_type, options) { 20 | if (_arg.isNaN) { 21 | _has_init = true; 22 | return; 23 | } 24 | 25 | if (_arg.isInfinite) { 26 | _is_negative = _arg.isNegative; 27 | _has_init = true; 28 | return; 29 | } 30 | 31 | _arg = _arg.toDouble(); 32 | 33 | if (_arg < 0) { 34 | _is_negative = true; 35 | _arg = -_arg; 36 | } 37 | 38 | var arg_str = 39 | _arg == _arg.truncate() ? _arg.toStringAsFixed(1) : _arg.toString(); 40 | 41 | var m1 = _number_rx.firstMatch(arg_str); 42 | if (m1 != null) { 43 | var int_part = m1.group(1)!; 44 | var fraction = m1.group(2)!; 45 | 46 | /* 47 | * Cases: 48 | * 1.2345 = 1.2345e0 -> [12345] e+0 d1 l5 49 | * 123.45 = 1.2345e2 -> [12345] e+2 d3 l5 50 | * 0.12345 = 1.2345e-1 -> [012345] e-1 d1 l6 51 | * 0.0012345 = 1.2345e-3 -> [00012345] e-3 d1 l8 52 | */ 53 | 54 | _decimal = int_part.length; 55 | _digits.addAll(int_part.split('').map(int.parse)); 56 | _digits.addAll(fraction.split('').map(int.parse)); 57 | 58 | if (int_part.length == 1) { 59 | if (int_part == '0') { 60 | var leading_zeroes_match = _leading_zeroes_rx.firstMatch(fraction); 61 | 62 | if (leading_zeroes_match != null) { 63 | var zeroes_count = leading_zeroes_match.group(1)!.length; 64 | // print("zeroes_count=${zeroes_count}"); 65 | _exponent = 66 | zeroes_count > 0 ? -(zeroes_count + 1) : zeroes_count - 1; 67 | } else { 68 | _exponent = 0; 69 | } 70 | } // else int_part != 0 71 | else { 72 | _exponent = 0; 73 | } 74 | } else { 75 | _exponent = int_part.length - 1; 76 | } 77 | } else { 78 | var m2 = _expo_rx.firstMatch(arg_str); 79 | if (m2 != null) { 80 | var int_part = m2.group(1)!; 81 | var fraction = m2.group(2)!; 82 | _exponent = int.parse(m2.group(3)!); 83 | 84 | if (_exponent > 0) { 85 | var diff = _exponent - fraction.length + 1; 86 | _decimal = _exponent + 1; 87 | _digits.addAll(int_part.split('').map(int.parse)); 88 | _digits.addAll(fraction.split('').map(int.parse)); 89 | _digits.addAll( 90 | Formatter.get_padding(diff, '0').split('').map(int.parse)); 91 | } else { 92 | var diff = int_part.length - _exponent - 1; 93 | _decimal = int_part.length; 94 | _digits.addAll( 95 | Formatter.get_padding(diff, '0').split('').map(int.parse)); 96 | _digits.addAll(int_part.split('').map(int.parse)); 97 | _digits.addAll(fraction.split('').map(int.parse)); 98 | } 99 | } // else something wrong 100 | } 101 | _has_init = true; 102 | //print("arg_str=${arg_str}"); 103 | //print("decimal=${_decimal}, exp=${_exponent}, digits=${_digits}"); 104 | } 105 | 106 | @override 107 | String asString() { 108 | var ret = ''; 109 | 110 | if (!_has_init) { 111 | return ret; 112 | } 113 | 114 | if (_output != null) { 115 | return _output!; 116 | } 117 | 118 | if (options['add_space'] && options['sign'] == '' && _arg >= 0) { 119 | options['sign'] = ' '; 120 | } 121 | 122 | if (_arg.isInfinite) { 123 | if (_arg.isNegative) { 124 | options['sign'] = '-'; 125 | } 126 | 127 | ret = 'inf'; 128 | options['padding_char'] = ' '; 129 | } 130 | 131 | if (_arg.isNaN) { 132 | ret = 'nan'; 133 | options['padding_char'] = ' '; 134 | } 135 | 136 | if (options['precision'] == -1) { 137 | // ignore: todo 138 | options['precision'] = 6; // TODO: make this configurable 139 | } else if (fmt_type == 'g' && options['precision'] == 0) { 140 | options['precision'] = 1; 141 | } 142 | 143 | if (_is_negative) { 144 | options['sign'] = '-'; 145 | } 146 | 147 | if (!(_arg.isInfinite || _arg.isNaN)) { 148 | if (fmt_type == 'e') { 149 | ret = asExponential(options['precision'], remove_trailing_zeros: false); 150 | } else if (fmt_type == 'f') { 151 | ret = asFixed(options['precision'], remove_trailing_zeros: false); 152 | } else { 153 | // type == g 154 | var _exp = _exponent; 155 | var sig_digs = options['precision']; 156 | // print("${_exp} ${sig_digs}"); 157 | if (-4 <= _exp && _exp < options['precision']) { 158 | sig_digs -= _decimal; 159 | var precision = max(options['precision'] - 1 - _exp, sig_digs); 160 | 161 | ret = asFixed(precision.toInt(), 162 | remove_trailing_zeros: !options['alternate_form']); 163 | } else { 164 | ret = asExponential(options['precision'] - 1, 165 | remove_trailing_zeros: !options['alternate_form']); 166 | } 167 | } 168 | } 169 | 170 | var min_chars = options['width']; 171 | var str_len = ret.length + options['sign'].length; 172 | var padding = ''; 173 | 174 | if (min_chars > str_len) { 175 | if (options['padding_char'] == '0' && !options['left_align']) { 176 | padding = Formatter.get_padding(min_chars - str_len, '0'); 177 | } else { 178 | padding = Formatter.get_padding(min_chars - str_len, ' '); 179 | } 180 | } 181 | 182 | if (options['left_align']) { 183 | ret = "${options['sign']}${ret}${padding}"; 184 | } else if (options['padding_char'] == '0') { 185 | ret = "${options['sign']}${padding}${ret}"; 186 | } else { 187 | ret = "${padding}${options['sign']}${ret}"; 188 | } 189 | 190 | if (options['is_upper']) { 191 | ret = ret.toUpperCase(); 192 | } 193 | 194 | return (_output = ret); 195 | } 196 | 197 | String asFixed(int precision, {bool remove_trailing_zeros = true}) { 198 | // precision is the number of decimal places after the decimal point to keep 199 | var offset = _decimal + precision - 1; 200 | var extra_zeroes = precision - (_digits.length - offset); 201 | 202 | if (extra_zeroes > 0) { 203 | _digits.addAll( 204 | Formatter.get_padding(extra_zeroes, '0').split('').map(int.parse)); 205 | } 206 | 207 | _round(offset + 1, offset); 208 | 209 | var ret = _digits.sublist(0, _decimal).fold('', (i, e) => '${i}${e}'); 210 | var trailing_digits = _digits.sublist(_decimal, _decimal + precision); 211 | if (remove_trailing_zeros) { 212 | trailing_digits = _remove_trailing_zeros(trailing_digits); 213 | } 214 | var trailing_zeroes = trailing_digits.fold('', (i, e) => '${i}${e}'); 215 | if (trailing_zeroes.isEmpty) { 216 | return ret; 217 | } 218 | ret = '${ret}.${trailing_zeroes}'; 219 | 220 | return ret; 221 | } 222 | 223 | String asExponential(int precision, {bool remove_trailing_zeros = true}) { 224 | var offset = _decimal - _exponent; 225 | 226 | var extra_zeroes = precision - (_digits.length - offset) + 1; 227 | 228 | if (extra_zeroes > 0) { 229 | _digits.addAll( 230 | Formatter.get_padding(extra_zeroes, '0').split('').map(int.parse)); 231 | } 232 | 233 | _round(offset + precision, offset); 234 | 235 | var ret = _digits[offset - 1].toString(); 236 | //print ("(${offset}, ${precision})${_digits}"); 237 | var trailing_digits = _digits.sublist(offset, offset + precision); 238 | // print ("trailing_digits=${trailing_digits}"); 239 | var _exp_str = _exponent.abs().toString(); 240 | 241 | if (_exponent < 10 && _exponent > -10) { 242 | _exp_str = '0${_exp_str}'; 243 | } 244 | 245 | _exp_str = (_exponent < 0) ? 'e-${_exp_str}' : 'e+${_exp_str}'; 246 | 247 | if (remove_trailing_zeros) { 248 | trailing_digits = _remove_trailing_zeros(trailing_digits); 249 | } 250 | 251 | if (trailing_digits.isNotEmpty) { 252 | ret += '.'; 253 | } 254 | 255 | ret = trailing_digits.fold(ret, (i, e) => '${i}${e}'); 256 | ret = '${ret}${_exp_str}'; 257 | 258 | return ret; 259 | } 260 | 261 | List _remove_trailing_zeros(List trailing_digits) { 262 | var nzeroes = 0; 263 | for (var i = trailing_digits.length - 1; i >= 0; i--) { 264 | if (trailing_digits[i] == 0) { 265 | nzeroes++; 266 | } else { 267 | break; 268 | } 269 | } 270 | return trailing_digits.sublist(0, trailing_digits.length - nzeroes); 271 | } 272 | 273 | /* 274 | rounding_offset: Where to start rounding from 275 | offset: where to end rounding 276 | */ 277 | void _round(var rounding_offset, var offset) { 278 | var carry = 0; 279 | 280 | if (rounding_offset >= _digits.length) { 281 | return; 282 | } 283 | // Round the digit after the precision 284 | var d = _digits[rounding_offset]; 285 | carry = d >= 5 ? 1 : 0; 286 | _digits[rounding_offset] = d % 10; 287 | rounding_offset -= 1; 288 | 289 | //propagate the carry 290 | while (carry > 0) { 291 | d = _digits[rounding_offset] + carry; 292 | if (rounding_offset == 0 && d > 9) { 293 | _digits.insert(0, 0); 294 | _decimal += 1; 295 | rounding_offset += 1; 296 | } 297 | carry = d < 10 ? 0 : 1; 298 | _digits[rounding_offset] = d % 10; 299 | rounding_offset -= 1; 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /lib/src/formatters/int_formatter.dart: -------------------------------------------------------------------------------- 1 | part of sprintf; 2 | 3 | class IntFormatter extends Formatter { 4 | int _arg; 5 | static const int MAX_INT = 0x1FFFFFFFFFFFFF; // javascript 53bit 6 | 7 | IntFormatter(this._arg, var fmt_type, var options) : super(fmt_type, options); 8 | 9 | @override 10 | String asString() { 11 | var ret = ''; 12 | var prefix = ''; 13 | 14 | var radix = fmt_type == 'x' ? 16 : (fmt_type == 'o' ? 8 : 10); 15 | 16 | if (_arg < 0) { 17 | if (radix == 10) { 18 | _arg = _arg.abs(); 19 | options['sign'] = '-'; 20 | } else { 21 | // sort of reverse twos complement 22 | _arg = (MAX_INT - (~_arg) & MAX_INT); 23 | } 24 | } 25 | 26 | ret = _arg.toRadixString(radix); 27 | 28 | if (options['alternate_form']) { 29 | if (radix == 16 && _arg != 0) { 30 | prefix = '0x'; 31 | } else if (radix == 8 && _arg != 0) { 32 | prefix = '0'; 33 | } 34 | if (options['sign'] == '+' && radix != 10) { 35 | options['sign'] = ''; 36 | } 37 | } 38 | 39 | // space "prefixes non-negative signed numbers with a space" 40 | if ((options['add_space'] && 41 | options['sign'] == '' && 42 | _arg > -1 && 43 | radix == 10)) { 44 | options['sign'] = ' '; 45 | } 46 | 47 | if (radix != 10) { 48 | options['sign'] = ''; 49 | } 50 | 51 | var padding = ''; 52 | var min_digits = options['precision']; 53 | var min_chars = options['width']; 54 | var num_length = ret.length; 55 | var sign_length = options['sign'].length; 56 | num str_len = 0; 57 | 58 | if (radix == 8 && min_chars <= min_digits) { 59 | num_length += prefix.length; 60 | } 61 | 62 | if (min_digits > num_length) { 63 | padding = Formatter.get_padding(min_digits - num_length, '0'); 64 | ret = '${padding}${ret}'; 65 | num_length = ret.length; 66 | padding = ''; 67 | } 68 | 69 | str_len = num_length + sign_length + prefix.length; 70 | if (min_chars > str_len) { 71 | if (options['padding_char'] == '0' && !options['left_align']) { 72 | padding = Formatter.get_padding(min_chars - str_len, '0'); 73 | } else { 74 | padding = Formatter.get_padding(min_chars - str_len, ' '); 75 | } 76 | } 77 | 78 | if (options['left_align']) { 79 | ret = "${options['sign']}${prefix}${ret}${padding}"; 80 | } else if (options['padding_char'] == '0') { 81 | ret = "${options['sign']}${prefix}${padding}${ret}"; 82 | } else { 83 | ret = "${padding}${options['sign']}${prefix}${ret}"; 84 | } 85 | 86 | if (options['is_upper']) { 87 | ret = ret.toUpperCase(); 88 | } 89 | 90 | return ret; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/formatters/string_formatter.dart: -------------------------------------------------------------------------------- 1 | part of sprintf; 2 | 3 | class StringFormatter extends Formatter { 4 | var _arg; 5 | 6 | StringFormatter(this._arg, var fmt_type, var options) 7 | : super(fmt_type, options) { 8 | options['padding_char'] = ' '; 9 | } 10 | @override 11 | String asString() { 12 | var ret = _arg.toString(); 13 | 14 | if (options['precision'] > -1 && options['precision'] <= ret.length) { 15 | ret = ret.substring(0, options['precision']); 16 | } 17 | 18 | if (options['width'] > -1) { 19 | int diff = (options['width'] - ret.length); 20 | 21 | if (diff > 0) { 22 | var padding = Formatter.get_padding(diff, options['padding_char']); 23 | if (!options['left_align']) { 24 | ret = '${padding}${ret}'; 25 | } else { 26 | ret = '${ret}${padding}'; 27 | } 28 | } 29 | } 30 | return ret; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/sprintf_impl.dart: -------------------------------------------------------------------------------- 1 | part of sprintf; 2 | 3 | typedef PrintFormatFormatter = Formatter Function(dynamic arg, dynamic options); 4 | //typedef Formatter PrintFormatFormatter(arg, options); 5 | 6 | class PrintFormat { 7 | static final RegExp specifier = RegExp( 8 | r'%(?:(\d+)\$)?([\+\-\#0 ]*)(\d+|\*)?(?:\.(\d+|\*))?([a-z%])', 9 | caseSensitive: false); 10 | static final RegExp uppercase_rx = RegExp(r'[A-Z]', caseSensitive: true); 11 | 12 | final Map _formatters = { 13 | 'i': (arg, options) => IntFormatter(arg, 'i', options), 14 | 'd': (arg, options) => IntFormatter(arg, 'd', options), 15 | 'x': (arg, options) => IntFormatter(arg, 'x', options), 16 | 'X': (arg, options) => IntFormatter(arg, 'x', options), 17 | 'o': (arg, options) => IntFormatter(arg, 'o', options), 18 | 'O': (arg, options) => IntFormatter(arg, 'o', options), 19 | 'e': (arg, options) => FloatFormatter(arg, 'e', options), 20 | 'E': (arg, options) => FloatFormatter(arg, 'e', options), 21 | 'f': (arg, options) => FloatFormatter(arg, 'f', options), 22 | 'F': (arg, options) => FloatFormatter(arg, 'f', options), 23 | 'g': (arg, options) => FloatFormatter(arg, 'g', options), 24 | 'G': (arg, options) => FloatFormatter(arg, 'g', options), 25 | 's': (arg, options) => StringFormatter(arg, 's', options), 26 | }; 27 | 28 | String call(String fmt, var args) { 29 | var ret = ''; 30 | 31 | var offset = 0; 32 | var arg_offset = 0; 33 | 34 | if (args is! List) { 35 | throw ArgumentError('Expecting list as second argument'); 36 | } 37 | 38 | for (var m in specifier.allMatches(fmt)) { 39 | var _parameter = m[1]; 40 | var _flags = m[2]!; 41 | var _width = m[3]; 42 | var _precision = m[4]; 43 | var _type = m[5]!; 44 | 45 | var _arg_str = ''; 46 | var _options = { 47 | 'is_upper': false, 48 | 'width': -1, 49 | 'precision': -1, 50 | 'length': -1, 51 | 'radix': 10, 52 | 'sign': '', 53 | 'specifier_type': _type, 54 | }; 55 | 56 | _parse_flags(_flags).forEach((var k, var v) { 57 | _options[k] = v; 58 | }); 59 | 60 | // The argument we want to deal with 61 | var _arg = _parameter == null ? null : args[int.parse(_parameter) - 1]; 62 | 63 | // parse width 64 | if (_width != null) { 65 | _options['width'] = 66 | (_width == '*' ? args[arg_offset++] : int.parse(_width)); 67 | } 68 | 69 | // parse precision 70 | if (_precision != null) { 71 | _options['precision'] = 72 | (_precision == '*' ? args[arg_offset++] : int.parse(_precision)); 73 | } 74 | 75 | // grab the argument we'll be dealing with 76 | if (_arg == null && _type != '%') { 77 | _arg = args[arg_offset++]; 78 | } 79 | 80 | _options['is_upper'] = uppercase_rx.hasMatch(_type); 81 | 82 | if (_type == '%') { 83 | if (_flags.isNotEmpty || _width != null || _precision != null) { 84 | throw Exception('"%" does not take any flags'); 85 | } 86 | _arg_str = '%'; 87 | } else if (_formatters.containsKey(_type)) { 88 | _arg_str = _formatters[_type]!(_arg, _options).asString(); 89 | } else { 90 | throw ArgumentError('Unknown format type ${_type}'); 91 | } 92 | 93 | // Add the pre-format string to the return 94 | ret += fmt.substring(offset, m.start); 95 | offset = m.end; 96 | 97 | ret += _arg_str; 98 | } 99 | 100 | return ret += fmt.substring(offset); 101 | } 102 | 103 | void register_specifier(String specifier, PrintFormatFormatter formatter) { 104 | _formatters[specifier] = formatter; 105 | } 106 | 107 | void unregistier_specifier(String specifier) { 108 | _formatters.remove(specifier); 109 | } 110 | 111 | Map _parse_flags(String flags) { 112 | return { 113 | 'sign': flags.contains('+') ? '+' : '', 114 | 'padding_char': flags.contains('0') ? '0' : ' ', 115 | 'add_space': flags.contains(' '), 116 | 'left_align': flags.contains('-'), 117 | 'alternate_form': flags.contains('#'), 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sprintf 2 | version: 7.0.0 3 | description: Dart implementation of sprintf. Provides simple printf like 4 | formatting such as sprintf("hello %s", ["world"]); 5 | homepage: https://github.com/Naddiseo/dart-sprintf 6 | 7 | environment: 8 | sdk: ">=2.12.0-0 <3.0.0" 9 | 10 | dev_dependencies: 11 | test: ^1.16.0-nullsafety.13 12 | lints: ^1.0.1 13 | -------------------------------------------------------------------------------- /test/sprintf_test.dart: -------------------------------------------------------------------------------- 1 | library sprintf_test; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | import 'package:sprintf/sprintf.dart'; 6 | 7 | part 'testing_data.dart'; 8 | 9 | void test_testdata() { 10 | expectedTestData.forEach((prefix, type_map) { 11 | group('"%$prefix Tests, ', () { 12 | type_map.forEach((type, expected_array) { 13 | var fmt = '|%${prefix}${type}|'; 14 | var input_array = expectedTestInputData[type]!; 15 | 16 | assert(input_array.length == expected_array.length); 17 | 18 | for (var i = 0; i < input_array.length - 1; i++) { 19 | var raw_input = input_array[i]; 20 | var expected = expected_array[i]; 21 | final input = raw_input is! List ? [raw_input] : raw_input; 22 | 23 | if (expected == '"throwsA"') { 24 | test('Expecting "${fmt}".format(${raw_input}) to throw', 25 | () => expect(() => sprintf(fmt, input), throwsA(anything))); 26 | } else { 27 | test('"${fmt}".format(${raw_input}) == "${expected}"', 28 | () => expect(sprintf(fmt, input), expected)); 29 | } 30 | } 31 | }); // type_map 32 | }); // group 33 | }); // _expected 34 | } 35 | 36 | void test_bug0001() { 37 | test('|%x|%X| 255', () => expect(sprintf('|%x|%X|', [255, 255]), '|ff|FF|')); 38 | } 39 | 40 | void test_bug0006a() { 41 | test('|%.0f| 5.466', () => expect(sprintf('|%.0f|', [5.466]), '|5|')); 42 | test('|%.0g| 5.466', () => expect(sprintf('|%.0g|', [5.466]), '|5|')); 43 | test('|%.0e| 5.466', () => expect(sprintf('|%.0e|', [5.466]), '|5e+00|')); 44 | } 45 | 46 | void test_bug0006b() { 47 | test('|%.2f| 5.466', () => expect(sprintf('|%.2f|', [5.466]), '|5.47|')); 48 | test('|%.2g| 5.466', () => expect(sprintf('|%.2g|', [5.466]), '|5.5|')); 49 | test('|%.2e| 5.466', () => expect(sprintf('|%.2e|', [5.466]), '|5.47e+00|')); 50 | } 51 | 52 | void test_bug0009() { 53 | test('|%.2f| 2.09846', () => expect(sprintf('|%.2f|', [2.09846]), '|2.10|')); 54 | } 55 | 56 | void test_bug0010() { 57 | test('|%.1f| 5.34', () => expect(sprintf('|%.1f|', [5.34]), '|5.3|')); 58 | test('|%.1f| 22.51', () => expect(sprintf('|%.1f|', [22.51]), '|22.5|')); 59 | test('|%.0f| 22.5', () => expect(sprintf('|%.0f|', [22.5]), '|23|')); 60 | test('|%.0f| 22.77', () => expect(sprintf('|%.0f|', [22.77]), '|23|')); 61 | } 62 | 63 | void test_javascript_decimal_limit() { 64 | test( 65 | '%d 9007199254740991', 66 | () => expect( 67 | sprintf('|%d|', [9007199254740991 + 0]), '|9007199254740991|')); 68 | //test('%d 9007199254740992', () => expect(sprintf('|%d|', [9007199254740991+1]), '|0|')); 69 | //test('%d 9007199254740993', () => expect(sprintf('|%d|', [9007199254740991+2]), '|1|')); 70 | 71 | test( 72 | '%x 9007199254740991', 73 | () => 74 | expect(sprintf('|%x|', [9007199254740991 + 0]), '|1fffffffffffff|')); 75 | //test('%x 9007199254740992', () => expect(sprintf('|%x|', [9007199254740991+1]), '|0|')); 76 | //test('%x 9007199254740993', () => expect(sprintf('|%x|', [9007199254740991+2]), '|1|')); 77 | 78 | test('%x -9007199254740991', 79 | () => expect(sprintf('|%x|', [-9007199254740991 + 0]), '|1|')); 80 | //test('%x -9007199254740992', () => expect(sprintf('|%x|', [-9007199254740991+1]), '|2|')); 81 | //test('%x -9007199254740993', () => expect(sprintf('|%x|', [-9007199254740991+2]), '|3|')); 82 | } 83 | 84 | void test_unsigned_neg_to_53bits() { 85 | test('|%x|%X| -0', () => expect(sprintf('|%x|%X|', [-0, -0]), '|0|0|')); 86 | test( 87 | '|%x|%X| -1', 88 | () => expect( 89 | sprintf('|%x|%X|', [-1, -1]), '|1fffffffffffff|1FFFFFFFFFFFFF|')); 90 | test( 91 | '|%x|%X| -2', 92 | () => expect( 93 | sprintf('|%x|%X|', [-2, -2]), '|1ffffffffffffe|1FFFFFFFFFFFFE|')); 94 | } 95 | 96 | void test_int_formatting() { 97 | test('|%+d|% d| 2', () => expect(sprintf('|%+d|% d|', [2, 2]), '|+2| 2|')); 98 | test('|%+d|% d| -2', () => expect(sprintf('|%+d|% d|', [-2, -2]), '|-2|-2|')); 99 | test( 100 | '|%+x|% X|%#x| -2', 101 | () => expect(sprintf('|%+x|% X|%#x|', [-2, -2, -2]), 102 | '|1ffffffffffffe|1FFFFFFFFFFFFE|0x1ffffffffffffe|')); 103 | } 104 | 105 | void test_large_exponents_e() { 106 | test('|%e| 1.79e+308', 107 | () => expect(sprintf('|%e|', [1.79e+308]), '|1.790000e+308|')); 108 | test('|%e| 1.79e-308', 109 | () => expect(sprintf('|%e|', [1.79e-308]), '|1.790000e-308|')); 110 | test('|%e| -1.79e+308', 111 | () => expect(sprintf('|%e|', [-1.79e+308]), '|-1.790000e+308|')); 112 | test('|%e| -1.79e-308', 113 | () => expect(sprintf('|%e|', [-1.79e-308]), '|-1.790000e-308|')); 114 | } 115 | 116 | void test_large_exponents_g() { 117 | test('|%g| 1.79e+308', 118 | () => expect(sprintf('|%g|', [1.79e+308]), '|1.79e+308|')); 119 | test('|%g| 1.79e-308', 120 | () => expect(sprintf('|%g|', [1.79e-308]), '|1.79e-308|')); 121 | test('|%g| -1.79e+308', 122 | () => expect(sprintf('|%g|', [-1.79e+308]), '|-1.79e+308|')); 123 | test('|%g| -1.79e-308', 124 | () => expect(sprintf('|%g|', [-1.79e-308]), '|-1.79e-308|')); 125 | } 126 | 127 | void test_large_exponents_f() { 128 | // ignore: todo 129 | // TODO: C's printf introduces errors after 20 decimal places 130 | test('|%f| 1.79e+308', 131 | () => expect(sprintf('|%.3f|', [1.79e+308]), '|1.79e+308|')); 132 | test('|%f| 1.79e-308', 133 | () => expect(sprintf('|%f|', [1.79e-308]), '|1.790000e-308|')); 134 | test('|%f| -1.79e+308', 135 | () => expect(sprintf('|%f|', [-1.79e+308]), '|-1.790000e+308|')); 136 | test('|%f| -1.79e-308', 137 | () => expect(sprintf('|%f|', [-1.79e-308]), '|-1.790000e-308|')); 138 | } 139 | 140 | void test_object_to_string() { 141 | var list = ['foo', 'bar']; 142 | test("|%s| ['foo', 'bar'].toString()", 143 | () => expect(sprintf('%s', [list]), '[foo, bar]')); 144 | } 145 | 146 | void test_round_bug0015() { 147 | var n = 1; 148 | test('|%.0f| 1', () => expect(sprintf('|%.0f|', [n]), '|1|')); 149 | test('|%.1f| 1', () => expect(sprintf('|%.1f|', [n]), '|1.0|')); 150 | test('|%.2f| 1', () => expect(sprintf('|%.2f|', [n]), '|1.00|')); 151 | 152 | test('|%.0f| 1.234', () => expect(sprintf('|%.0f|', [1.234]), '|1|')); 153 | test('|%.1f| 1.234', () => expect(sprintf('|%.1f|', [1.234]), '|1.2|')); 154 | test('|%.2f| 1.234', () => expect(sprintf('|%.2f|', [1.234]), '|1.23|')); 155 | 156 | test('|%.0f| 1.235', () => expect(sprintf('|%.0f|', [1.235]), '|1|')); 157 | test('|%.1f| 1.235', () => expect(sprintf('|%.1f|', [1.235]), '|1.2|')); 158 | test('|%.2f| 1.235', () => expect(sprintf('|%.2f|', [1.235]), '|1.24|')); 159 | } 160 | 161 | void test_bug0018() { 162 | test( 163 | '|%10.4f| 1.0', () => expect(sprintf('|%10.4f|', [1.0]), '| 1.0000|')); 164 | } 165 | 166 | void test_bug0022() { 167 | test('|%2\$d %2\$d %1\$d|', 168 | () => expect(sprintf('|%2\$d %2\$d %1\$d|', [5, 10]), '|10 10 5|')); 169 | 170 | // these next two are from the sprintf manual, and should print the same 171 | test('|%*d|', () => expect(sprintf('|%*d|', [5, 10]), '| 10|')); 172 | test('|%2\$*1\$d|', () => expect(sprintf('|%*d|', [5, 10]), '| 10|')); 173 | } 174 | 175 | void test_bug0033() { 176 | var inf = 1.0 / 0.0; 177 | var nan = 0.0 / 0.0; 178 | test( 179 | '|%g %G| Infinity', 180 | () => expect(sprintf('|%g %g %G %G|', [inf, -inf, inf, -inf]), 181 | '|inf -inf INF -INF|')); 182 | test( 183 | '|%g %G| NaN', () => expect(sprintf('|%g %G|', [nan, nan]), '|nan NAN|')); 184 | 185 | test( 186 | '|%f %F| Infinity', 187 | () => expect(sprintf('|%f %f %F %F|', [inf, -inf, inf, -inf]), 188 | '|inf -inf INF -INF|')); 189 | test( 190 | '|%f %F| NaN', () => expect(sprintf('|%f %F|', [nan, nan]), '|nan NAN|')); 191 | } 192 | 193 | void main() { 194 | test_bug0022(); 195 | 196 | test('|%6.6g -1.79e+20', 197 | () => expect(sprintf('|%6.6g|', [-1.79E+20]), '|-1.79e+20|')); 198 | test('|%6.6G -1.79e+20', 199 | () => expect(sprintf('|%6.6G|', [-1.79E+20]), '|-1.79E+20|')); 200 | test_bug0018(); 201 | 202 | //test_bug0009(); 203 | //test_bug0010(); 204 | //test('|%f| 1.79E+308', () => expect(sprintf('|%f|', [1.79e+308]), '|1.79e+308|')); 205 | test_unsigned_neg_to_53bits(); 206 | test_int_formatting(); 207 | 208 | test_javascript_decimal_limit(); 209 | if (true) { 210 | test_testdata(); 211 | test_large_exponents_e(); 212 | test_large_exponents_g(); 213 | //test_large_exponents_f(); 214 | 215 | test_bug0001(); 216 | test_bug0006a(); 217 | test_bug0006b(); 218 | 219 | test_bug0009(); 220 | test_bug0010(); 221 | 222 | test_object_to_string(); 223 | } 224 | test_bug0033(); 225 | } 226 | -------------------------------------------------------------------------------- /tool/gentests.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations, permutations 2 | from pprint import pformat 3 | from ctypes import * 4 | import os 5 | 6 | sprintf = CDLL('libc.so.6').sprintf 7 | 8 | JS_MAX=9007199254740991 9 | JS_MIN=-9007199254740991 10 | float_tests = [123.0, -123.0, 0.0, 1.79E+20, -1.79E+20, 1.79E-20, -1.79E-20, 5.4444466, 2.0999995] 11 | int_tests = [123, -123, 0, JS_MAX, JS_MIN] 12 | 13 | _test_suite_input = { 14 | '%': [1, -1, 'a', 'asdf', 123], 15 | 'E': float_tests, 16 | 'F': float_tests, 17 | 'G': float_tests, 18 | 'X': int_tests, 19 | 'd': int_tests, 20 | 'e': float_tests, 21 | 'f': float_tests, 22 | 'g': float_tests, 23 | 'o': int_tests, 24 | 's': ['', 'Hello World'], 25 | 'x': int_tests 26 | } 27 | 28 | val = { 29 | 'd' : '', 30 | 'f' : '', 31 | 'F' : '', 32 | 'e' : '', 33 | 'E' : '', 34 | 'g' : '', 35 | 'G' : '', 36 | 'x' : '', 37 | 'X' : '', 38 | 'o' : '', 39 | 's' : '', 40 | '%' : '', 41 | } 42 | 43 | def get_prefix(): 44 | yield '' 45 | for i in xrange(0, 7): 46 | for l in combinations([' ', '-', '+', '#', '0', '6', '.6'], r = i): 47 | yield ''.join(l) 48 | 49 | expected = {k : val.copy() for k in get_prefix()} 50 | 51 | new_expected = {} 52 | 53 | for prefix, type_map in expected.items(): 54 | new_expected[prefix] = {} 55 | for fmt_type, expected in type_map.items(): 56 | 57 | new_expected[prefix][fmt_type] = [] 58 | 59 | pyfmt = "|{{:{}{}}}|".format(prefix.replace('-', '<'), fmt_type) 60 | cfmt = "|%{}{}|".format(prefix, fmt_type) 61 | input_array = _test_suite_input[fmt_type] 62 | 63 | if fmt_type in 'doxX': 64 | cfmt = cfmt.replace('d', 'lld').replace('o', 'llo').replace('x', 'llx').replace('X', 'llX') 65 | 66 | for input_data in input_array: 67 | ret = create_string_buffer(1024) 68 | try: 69 | if fmt_type == '%': 70 | if len(prefix) > 0: 71 | new_expected[prefix][fmt_type].append('"throwsA"') 72 | #print cfmt, 'throws' 73 | continue 74 | else: 75 | sprintf(ret, cfmt, input_data) 76 | #print cfmt, ret.value 77 | new_expected[prefix][fmt_type].append(ret.value) 78 | 79 | elif fmt_type in 'doxX': 80 | if fmt_type in 'oxX': 81 | wrapped = input_data&JS_MAX 82 | else: 83 | wrapped = input_data 84 | sprintf(ret, cfmt, c_int64(wrapped)) 85 | new_expected[prefix][fmt_type].append(ret.value) 86 | 87 | #if 'x' in fmt_type and input_data == -123: 88 | # print "OUTPUT: {} {}".format(cfmt, ret.value) 89 | 90 | elif fmt_type in 'efgEFG': 91 | sprintf(ret, cfmt, c_double(input_data)) 92 | #print cfmt, ret.value 93 | new_expected[prefix][fmt_type].append(ret.value) 94 | 95 | else: 96 | ret = create_string_buffer(1024) 97 | sprintf(ret, cfmt, input_data) 98 | #print cfmt, ret.value 99 | new_expected[prefix][fmt_type].append(ret.value) 100 | except ValueError: 101 | raise 102 | 103 | this_dir = os.path.dirname(os.path.abspath(__file__)) 104 | test_data_path = os.path.abspath(os.path.join(this_dir, '..', 'test', 'testing_data.dart')) 105 | 106 | def prettify(expr): 107 | formatted_data = pformat(expr) 108 | 109 | formatted_data = formatted_data.replace(']}', ']\n }') 110 | formatted_data = formatted_data.replace(": {'%':", ": {\n '%':") 111 | formatted_data = formatted_data.replace('}}', '}\n}') 112 | formatted_data = formatted_data.replace("{'': {", "{\n '': {") 113 | formatted_data = formatted_data.replace("'throws'", "throws") 114 | 115 | return formatted_data 116 | 117 | with open(test_data_path, 'w') as fp: 118 | 119 | fp.write('part of sprintf_test;\n') 120 | fp.write('var expectedTestData = ') 121 | fp.write(prettify(new_expected)) 122 | fp.write(';\n') 123 | 124 | fp.write('var expectedTestInputData = ') 125 | fp.write(prettify(_test_suite_input)) 126 | fp.write(';\n') 127 | 128 | 129 | --------------------------------------------------------------------------------