sprintf.js 12 KB

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