sprintf.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.string.sprintf"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.string.sprintf"] = true;
  8. dojo.provide("dojox.string.sprintf");
  9. dojo.require("dojox.string.tokenize");
  10. dojox.string.sprintf = function(/*String*/ format, /*mixed...*/ filler){
  11. for(var args = [], i = 1; i < arguments.length; i++){
  12. args.push(arguments[i]);
  13. }
  14. var formatter = new dojox.string.sprintf.Formatter(format);
  15. return formatter.format.apply(formatter, args);
  16. }
  17. dojox.string.sprintf.Formatter = function(/*String*/ format){
  18. var tokens = [];
  19. this._mapped = false;
  20. this._format = format;
  21. this._tokens = dojox.string.tokenize(format, this._re, this._parseDelim, this);
  22. }
  23. dojo.extend(dojox.string.sprintf.Formatter, {
  24. _re: /\%(?:\(([\w_]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?[hlL]?([\%scdeEfFgGiouxX])/g,
  25. _parseDelim: function(mapping, intmapping, flags, minWidth, period, precision, specifier){
  26. if(mapping){
  27. this._mapped = true;
  28. }
  29. return {
  30. mapping: mapping,
  31. intmapping: intmapping,
  32. flags: flags,
  33. _minWidth: minWidth, // May be dependent on parameters
  34. period: period,
  35. _precision: precision, // May be dependent on parameters
  36. specifier: specifier
  37. };
  38. },
  39. _specifiers: {
  40. b: {
  41. base: 2,
  42. isInt: true
  43. },
  44. o: {
  45. base: 8,
  46. isInt: true
  47. },
  48. x: {
  49. base: 16,
  50. isInt: true
  51. },
  52. X: {
  53. extend: ["x"],
  54. toUpper: true
  55. },
  56. d: {
  57. base: 10,
  58. isInt: true
  59. },
  60. i: {
  61. extend: ["d"]
  62. },
  63. u: {
  64. extend: ["d"],
  65. isUnsigned: true
  66. },
  67. c: {
  68. setArg: function(token){
  69. if(!isNaN(token.arg)){
  70. var num = parseInt(token.arg);
  71. if(num < 0 || num > 127){
  72. throw new Error("invalid character code passed to %c in sprintf");
  73. }
  74. token.arg = isNaN(num) ? "" + num : String.fromCharCode(num);
  75. }
  76. }
  77. },
  78. s: {
  79. setMaxWidth: function(token){
  80. token.maxWidth = (token.period == ".") ? token.precision : -1;
  81. }
  82. },
  83. e: {
  84. isDouble: true,
  85. doubleNotation: "e"
  86. },
  87. E: {
  88. extend: ["e"],
  89. toUpper: true
  90. },
  91. f: {
  92. isDouble: true,
  93. doubleNotation: "f"
  94. },
  95. F: {
  96. extend: ["f"]
  97. },
  98. g: {
  99. isDouble: true,
  100. doubleNotation: "g"
  101. },
  102. G: {
  103. extend: ["g"],
  104. toUpper: true
  105. }
  106. },
  107. format: function(/*mixed...*/ filler){
  108. if(this._mapped && typeof filler != "object"){
  109. throw new Error("format requires a mapping");
  110. }
  111. var str = "";
  112. var position = 0;
  113. for(var i = 0, token; i < this._tokens.length; i++){
  114. token = this._tokens[i];
  115. if(typeof token == "string"){
  116. str += token;
  117. }else{
  118. if(this._mapped){
  119. if(typeof filler[token.mapping] == "undefined"){
  120. throw new Error("missing key " + token.mapping);
  121. }
  122. token.arg = filler[token.mapping];
  123. }else{
  124. if(token.intmapping){
  125. var position = parseInt(token.intmapping) - 1;
  126. }
  127. if(position >= arguments.length){
  128. throw new Error("got " + arguments.length + " printf arguments, insufficient for '" + this._format + "'");
  129. }
  130. token.arg = arguments[position++];
  131. }
  132. if(!token.compiled){
  133. token.compiled = true;
  134. token.sign = "";
  135. token.zeroPad = false;
  136. token.rightJustify = false;
  137. token.alternative = false;
  138. var flags = {};
  139. for(var fi = token.flags.length; fi--;){
  140. var flag = token.flags.charAt(fi);
  141. flags[flag] = true;
  142. switch(flag){
  143. case " ":
  144. token.sign = " ";
  145. break;
  146. case "+":
  147. token.sign = "+";
  148. break;
  149. case "0":
  150. token.zeroPad = (flags["-"]) ? false : true;
  151. break;
  152. case "-":
  153. token.rightJustify = true;
  154. token.zeroPad = false;
  155. break;
  156. case "\#":
  157. token.alternative = true;
  158. break;
  159. default:
  160. throw Error("bad formatting flag '" + token.flags.charAt(fi) + "'");
  161. }
  162. }
  163. token.minWidth = (token._minWidth) ? parseInt(token._minWidth) : 0;
  164. token.maxWidth = -1;
  165. token.toUpper = false;
  166. token.isUnsigned = false;
  167. token.isInt = false;
  168. token.isDouble = false;
  169. token.precision = 1;
  170. if(token.period == '.'){
  171. if(token._precision){
  172. token.precision = parseInt(token._precision);
  173. }else{
  174. token.precision = 0;
  175. }
  176. }
  177. var mixins = this._specifiers[token.specifier];
  178. if(typeof mixins == "undefined"){
  179. throw new Error("unexpected specifier '" + token.specifier + "'");
  180. }
  181. if(mixins.extend){
  182. dojo.mixin(mixins, this._specifiers[mixins.extend]);
  183. delete mixins.extend;
  184. }
  185. dojo.mixin(token, mixins);
  186. }
  187. if(typeof token.setArg == "function"){
  188. token.setArg(token);
  189. }
  190. if(typeof token.setMaxWidth == "function"){
  191. token.setMaxWidth(token);
  192. }
  193. if(token._minWidth == "*"){
  194. if(this._mapped){
  195. throw new Error("* width not supported in mapped formats");
  196. }
  197. token.minWidth = parseInt(arguments[position++]);
  198. if(isNaN(token.minWidth)){
  199. throw new Error("the argument for * width at position " + position + " is not a number in " + this._format);
  200. }
  201. // negative width means rightJustify
  202. if (token.minWidth < 0) {
  203. token.rightJustify = true;
  204. token.minWidth = -token.minWidth;
  205. }
  206. }
  207. if(token._precision == "*" && token.period == "."){
  208. if(this._mapped){
  209. throw new Error("* precision not supported in mapped formats");
  210. }
  211. token.precision = parseInt(arguments[position++]);
  212. if(isNaN(token.precision)){
  213. throw Error("the argument for * precision at position " + position + " is not a number in " + this._format);
  214. }
  215. // negative precision means unspecified
  216. if (token.precision < 0) {
  217. token.precision = 1;
  218. token.period = '';
  219. }
  220. }
  221. if(token.isInt){
  222. // a specified precision means no zero padding
  223. if(token.period == '.'){
  224. token.zeroPad = false;
  225. }
  226. this.formatInt(token);
  227. }else if(token.isDouble){
  228. if(token.period != '.'){
  229. token.precision = 6;
  230. }
  231. this.formatDouble(token);
  232. }
  233. this.fitField(token);
  234. str += "" + token.arg;
  235. }
  236. }
  237. return str;
  238. },
  239. _zeros10: '0000000000',
  240. _spaces10: ' ',
  241. formatInt: function(token) {
  242. var i = parseInt(token.arg);
  243. if(!isFinite(i)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
  244. // allow this only if arg is number
  245. if(typeof token.arg != "number"){
  246. throw new Error("format argument '" + token.arg + "' not an integer; parseInt returned " + i);
  247. }
  248. //return '' + i;
  249. i = 0;
  250. }
  251. // if not base 10, make negatives be positive
  252. // otherwise, (-10).toString(16) is '-a' instead of 'fffffff6'
  253. if(i < 0 && (token.isUnsigned || token.base != 10)){
  254. i = 0xffffffff + i + 1;
  255. }
  256. if(i < 0){
  257. token.arg = (- i).toString(token.base);
  258. this.zeroPad(token);
  259. token.arg = "-" + token.arg;
  260. }else{
  261. token.arg = i.toString(token.base);
  262. // need to make sure that argument 0 with precision==0 is formatted as ''
  263. if(!i && !token.precision){
  264. token.arg = "";
  265. }else{
  266. this.zeroPad(token);
  267. }
  268. if(token.sign){
  269. token.arg = token.sign + token.arg;
  270. }
  271. }
  272. if(token.base == 16){
  273. if(token.alternative){
  274. token.arg = '0x' + token.arg;
  275. }
  276. token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
  277. }
  278. if(token.base == 8){
  279. if(token.alternative && token.arg.charAt(0) != '0'){
  280. token.arg = '0' + token.arg;
  281. }
  282. }
  283. },
  284. formatDouble: function(token) {
  285. var f = parseFloat(token.arg);
  286. if(!isFinite(f)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
  287. // allow this only if arg is number
  288. if(typeof token.arg != "number"){
  289. throw new Error("format argument '" + token.arg + "' not a float; parseFloat returned " + f);
  290. }
  291. // C99 says that for 'f':
  292. // infinity -> '[-]inf' or '[-]infinity' ('[-]INF' or '[-]INFINITY' for 'F')
  293. // NaN -> a string starting with 'nan' ('NAN' for 'F')
  294. // this is not commonly implemented though.
  295. //return '' + f;
  296. f = 0;
  297. }
  298. switch(token.doubleNotation) {
  299. case 'e': {
  300. token.arg = f.toExponential(token.precision);
  301. break;
  302. }
  303. case 'f': {
  304. token.arg = f.toFixed(token.precision);
  305. break;
  306. }
  307. case 'g': {
  308. // C says use 'e' notation if exponent is < -4 or is >= prec
  309. // ECMAScript for toPrecision says use exponential notation if exponent is >= prec,
  310. // though step 17 of toPrecision indicates a test for < -6 to force exponential.
  311. if(Math.abs(f) < 0.0001){
  312. //print("forcing exponential notation for f=" + f);
  313. token.arg = f.toExponential(token.precision > 0 ? token.precision - 1 : token.precision);
  314. }else{
  315. token.arg = f.toPrecision(token.precision);
  316. }
  317. // In C, unlike 'f', 'gG' removes trailing 0s from fractional part, unless alternative format flag ("#").
  318. // But ECMAScript formats toPrecision as 0.00100000. So remove trailing 0s.
  319. if(!token.alternative){
  320. //print("replacing trailing 0 in '" + s + "'");
  321. token.arg = token.arg.replace(/(\..*[^0])0*/, "$1");
  322. // if fractional part is entirely 0, remove it and decimal point
  323. token.arg = token.arg.replace(/\.0*e/, 'e').replace(/\.0$/,'');
  324. }
  325. break;
  326. }
  327. default: throw new Error("unexpected double notation '" + token.doubleNotation + "'");
  328. }
  329. // C says that exponent must have at least two digits.
  330. // But ECMAScript does not; toExponential results in things like "1.000000e-8" and "1.000000e+8".
  331. // Note that s.replace(/e([\+\-])(\d)/, "e$10$2") won't work because of the "$10" instead of "$1".
  332. // And replace(re, func) isn't supported on IE50 or Safari1.
  333. token.arg = token.arg.replace(/e\+(\d)$/, "e+0$1").replace(/e\-(\d)$/, "e-0$1");
  334. // Ensure a '0' before the period.
  335. // Opera implements (0.001).toString() as '0.001', but (0.001).toFixed(1) is '.001'
  336. if(dojo.isOpera){
  337. token.arg = token.arg.replace(/^\./, '0.');
  338. }
  339. // if alt, ensure a decimal point
  340. if(token.alternative){
  341. token.arg = token.arg.replace(/^(\d+)$/,"$1.");
  342. token.arg = token.arg.replace(/^(\d+)e/,"$1.e");
  343. }
  344. if(f >= 0 && token.sign){
  345. token.arg = token.sign + token.arg;
  346. }
  347. token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
  348. },
  349. zeroPad: function(token, /*Int*/ length) {
  350. length = (arguments.length == 2) ? length : token.precision;
  351. if(typeof token.arg != "string"){
  352. token.arg = "" + token.arg;
  353. }
  354. var tenless = length - 10;
  355. while(token.arg.length < tenless){
  356. token.arg = (token.rightJustify) ? token.arg + this._zeros10 : this._zeros10 + token.arg;
  357. }
  358. var pad = length - token.arg.length;
  359. token.arg = (token.rightJustify) ? token.arg + this._zeros10.substring(0, pad) : this._zeros10.substring(0, pad) + token.arg;
  360. },
  361. fitField: function(token) {
  362. if(token.maxWidth >= 0 && token.arg.length > token.maxWidth){
  363. return token.arg.substring(0, token.maxWidth);
  364. }
  365. if(token.zeroPad){
  366. this.zeroPad(token, token.minWidth);
  367. return;
  368. }
  369. this.spacePad(token);
  370. },
  371. spacePad: function(token, /*Int*/ length) {
  372. length = (arguments.length == 2) ? length : token.minWidth;
  373. if(typeof token.arg != 'string'){
  374. token.arg = '' + token.arg;
  375. }
  376. var tenless = length - 10;
  377. while(token.arg.length < tenless){
  378. token.arg = (token.rightJustify) ? token.arg + this._spaces10 : this._spaces10 + token.arg;
  379. }
  380. var pad = length - token.arg.length;
  381. token.arg = (token.rightJustify) ? token.arg + this._spaces10.substring(0, pad) : this._spaces10.substring(0, pad) + token.arg;
  382. }
  383. });
  384. }