/lib/log4js.js

https://github.com/couchone/log4js-node · JavaScript · 731 lines · 513 code · 73 blank · 145 comment · 90 complexity · 2a4eb7cc800c6049eac8c212b7e8b878 MD5 · raw file

  1. /*
  2. * Licensed under the Apache License, Version 2.0 (the "License");
  3. * you may not use this file except in compliance with the License.
  4. * You may obtain a copy of the License at
  5. *
  6. * http://www.apache.org/licenses/LICENSE-2.0
  7. *
  8. * Unless required by applicable law or agreed to in writing, software
  9. * distributed under the License is distributed on an "AS IS" BASIS,
  10. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. * See the License for the specific language governing permissions and
  12. * limitations under the License.
  13. */
  14. /*jsl:option explicit*/
  15. /**
  16. * @fileoverview log4js is a library to log in JavaScript in similar manner
  17. * than in log4j for Java. The API should be nearly the same.
  18. *
  19. * This file contains all log4js code and is the only file required for logging.
  20. *
  21. * <h3>Example:</h3>
  22. * <pre>
  23. * var logging = require('log4js-node')();
  24. * //add an appender that logs all messages to stdout.
  25. * logging.addAppender(logging.consoleAppender());
  26. * //add an appender that logs "some-category" to a file
  27. * logging.addAppender(logging.fileAppender("file.log"), "some-category");
  28. * //get a logger
  29. * var log = logging.getLogger("some-category");
  30. * log.setLevel(logging.levels.TRACE); //set the Level
  31. *
  32. * ...
  33. *
  34. * //call the log
  35. * log.trace("trace me" );
  36. * </pre>
  37. *
  38. * @version 1.0
  39. * @author Stephan Strittmatter - http://jroller.com/page/stritti
  40. * @author Seth Chisamore - http://www.chisamore.com
  41. * @since 2005-05-20
  42. * @static
  43. * Website: http://log4js.berlios.de
  44. */
  45. var events = require('events'),
  46. path = require('path'),
  47. sys = require('sys'),
  48. DEFAULT_CATEGORY = '[default]',
  49. ALL_CATEGORIES = '[all]',
  50. appenders = {},
  51. loggers = {},
  52. levels = {
  53. ALL: new Level(Number.MIN_VALUE, "ALL", "grey"),
  54. TRACE: new Level(5000, "TRACE", "blue"),
  55. DEBUG: new Level(10000, "DEBUG", "cyan"),
  56. INFO: new Level(20000, "INFO", "green"),
  57. WARN: new Level(30000, "WARN", "yellow"),
  58. ERROR: new Level(40000, "ERROR", "red"),
  59. FATAL: new Level(50000, "FATAL", "magenta"),
  60. OFF: new Level(Number.MAX_VALUE, "OFF", "grey")
  61. },
  62. appenderMakers = {
  63. "file": function(config, fileAppender) {
  64. var layout;
  65. if (config.layout) {
  66. layout = layoutMakers[config.layout.type](config.layout);
  67. }
  68. return fileAppender(config.filename, layout, config.maxLogSize, config.backups, config.pollInterval);
  69. },
  70. "console": function(config, fileAppender, consoleAppender) {
  71. var layout;
  72. if (config.layout) {
  73. layout = layoutMakers[config.layout.type](config.layout);
  74. }
  75. return consoleAppender(layout);
  76. },
  77. "logLevelFilter": function(config, fileAppender, consoleAppender) {
  78. var appender = appenderMakers[config.appender.type](config.appender, fileAppender, consoleAppender);
  79. return logLevelFilter(config.level, appender);
  80. }
  81. },
  82. layoutMakers = {
  83. "messagePassThrough": function() { return messagePassThroughLayout; },
  84. "basic": function() { return basicLayout; },
  85. "pattern": function (config) {
  86. var pattern = config.pattern || undefined;
  87. return patternLayout(pattern);
  88. }
  89. };
  90. /**
  91. * Get a logger instance. Instance is cached on categoryName level.
  92. * @param {String} categoryName name of category to log to.
  93. * @return {Logger} instance of logger for the category
  94. * @static
  95. */
  96. function getLogger (categoryName) {
  97. // Use default logger if categoryName is not specified or invalid
  98. if (!(typeof categoryName == "string")) {
  99. categoryName = DEFAULT_CATEGORY;
  100. }
  101. var appenderList;
  102. if (!loggers[categoryName]) {
  103. // Create the logger for this name if it doesn't already exist
  104. loggers[categoryName] = new Logger(categoryName);
  105. if (appenders[categoryName]) {
  106. appenderList = appenders[categoryName];
  107. appenderList.forEach(function(appender) {
  108. loggers[categoryName].addListener("log", appender);
  109. });
  110. }
  111. if (appenders[ALL_CATEGORIES]) {
  112. appenderList = appenders[ALL_CATEGORIES];
  113. appenderList.forEach(function(appender) {
  114. loggers[categoryName].addListener("log", appender);
  115. });
  116. }
  117. }
  118. return loggers[categoryName];
  119. }
  120. /**
  121. * args are appender, then zero or more categories
  122. */
  123. function addAppender () {
  124. var args = Array.prototype.slice.call(arguments);
  125. var appender = args.shift();
  126. if (args.length == 0 || args[0] === undefined) {
  127. args = [ ALL_CATEGORIES ];
  128. }
  129. //argument may already be an array
  130. if (args[0].forEach) {
  131. args = args[0];
  132. }
  133. args.forEach(function(category) {
  134. if (!appenders[category]) {
  135. appenders[category] = [];
  136. }
  137. appenders[category].push(appender);
  138. if (category === ALL_CATEGORIES) {
  139. for (var logger in loggers) {
  140. if (loggers.hasOwnProperty(logger)) {
  141. loggers[logger].addListener("log", appender);
  142. }
  143. }
  144. } else if (loggers[category]) {
  145. loggers[category].addListener("log", appender);
  146. }
  147. });
  148. appenders.count = appenders.count ? appenders.count++ : 1;
  149. }
  150. function clearAppenders () {
  151. appenders = {};
  152. for (var logger in loggers) {
  153. if (loggers.hasOwnProperty(logger)) {
  154. loggers[logger].removeAllListeners("log");
  155. }
  156. }
  157. }
  158. function configureAppenders(appenderList, fileAppender, consoleAppender) {
  159. clearAppenders();
  160. if (appenderList) {
  161. appenderList.forEach(function(appenderConfig) {
  162. var appender = appenderMakers[appenderConfig.type](appenderConfig, fileAppender, consoleAppender);
  163. if (appender) {
  164. addAppender(appender, appenderConfig.category);
  165. } else {
  166. throw new Error("log4js configuration problem for "+sys.inspect(appenderConfig));
  167. }
  168. });
  169. } else {
  170. addAppender(consoleAppender);
  171. }
  172. }
  173. function configureLevels(levels) {
  174. if (levels) {
  175. for (var category in levels) {
  176. if (levels.hasOwnProperty(category)) {
  177. getLogger(category).setLevel(levels[category]);
  178. }
  179. }
  180. }
  181. }
  182. function Level(level, levelStr, colour) {
  183. this.level = level;
  184. this.levelStr = levelStr;
  185. this.colour = colour;
  186. }
  187. /**
  188. * converts given String to corresponding Level
  189. * @param {String} sArg String value of Level
  190. * @param {Log4js.Level} defaultLevel default Level, if no String representation
  191. * @return Level object
  192. * @type Log4js.Level
  193. */
  194. Level.toLevel = function(sArg, defaultLevel) {
  195. if (sArg === null) {
  196. return defaultLevel;
  197. }
  198. if (typeof sArg == "string") {
  199. var s = sArg.toUpperCase();
  200. if (levels[s]) {
  201. return levels[s];
  202. }
  203. }
  204. return defaultLevel;
  205. };
  206. Level.prototype.toString = function() {
  207. return this.levelStr;
  208. };
  209. Level.prototype.isLessThanOrEqualTo = function(otherLevel) {
  210. return this.level <= otherLevel.level;
  211. };
  212. Level.prototype.isGreaterThanOrEqualTo = function(otherLevel) {
  213. return this.level >= otherLevel.level;
  214. };
  215. /**
  216. * Models a logging event.
  217. * @constructor
  218. * @param {String} categoryName name of category
  219. * @param {Log4js.Level} level level of message
  220. * @param {String} message message to log
  221. * @param {Log4js.Logger} logger the associated logger
  222. * @author Seth Chisamore
  223. */
  224. function LoggingEvent (categoryName, level, message, exception, logger) {
  225. this.startTime = new Date();
  226. this.categoryName = categoryName;
  227. this.message = message;
  228. this.level = level;
  229. this.logger = logger;
  230. if (exception && exception.message && exception.name) {
  231. this.exception = exception;
  232. } else if (exception) {
  233. this.exception = new Error(sys.inspect(exception));
  234. }
  235. }
  236. /**
  237. * Logger to log messages.
  238. * use {@see Log4js#getLogger(String)} to get an instance.
  239. * @constructor
  240. * @param name name of category to log to
  241. * @author Stephan Strittmatter
  242. */
  243. function Logger (name, level) {
  244. this.category = name || DEFAULT_CATEGORY;
  245. this.level = Level.toLevel(level, levels.TRACE);
  246. }
  247. sys.inherits(Logger, events.EventEmitter);
  248. Logger.prototype.setLevel = function(level) {
  249. this.level = Level.toLevel(level, levels.TRACE);
  250. };
  251. Logger.prototype.log = function(logLevel, message, exception) {
  252. var loggingEvent = new LoggingEvent(this.category, logLevel, message, exception, this);
  253. this.emit("log", loggingEvent);
  254. };
  255. Logger.prototype.isLevelEnabled = function(otherLevel) {
  256. return this.level.isLessThanOrEqualTo(otherLevel);
  257. };
  258. ['Trace','Debug','Info','Warn','Error','Fatal'].forEach(
  259. function(levelString) {
  260. var level = Level.toLevel(levelString);
  261. Logger.prototype['is'+levelString+'Enabled'] = function() {
  262. return this.isLevelEnabled(level);
  263. };
  264. Logger.prototype[levelString.toLowerCase()] = function (message, exception) {
  265. if (this.isLevelEnabled(level)) {
  266. this.log(level, message, exception);
  267. }
  268. };
  269. }
  270. );
  271. /**
  272. * Get the default logger instance.
  273. * @return {Logger} instance of default logger
  274. * @static
  275. */
  276. function getDefaultLogger () {
  277. return getLogger(DEFAULT_CATEGORY);
  278. }
  279. function logLevelFilter (levelString, appender) {
  280. var level = Level.toLevel(levelString);
  281. return function(logEvent) {
  282. if (logEvent.level.isGreaterThanOrEqualTo(level)) {
  283. appender(logEvent);
  284. }
  285. }
  286. }
  287. /**
  288. * BasicLayout is a simple layout for storing the logs. The logs are stored
  289. * in following format:
  290. * <pre>
  291. * [startTime] [logLevel] categoryName - message\n
  292. * </pre>
  293. *
  294. * @author Stephan Strittmatter
  295. */
  296. function basicLayout (loggingEvent) {
  297. var timestampLevelAndCategory = '[' + loggingEvent.startTime.toFormattedString() + '] ';
  298. timestampLevelAndCategory += '[' + loggingEvent.level.toString() + '] ';
  299. timestampLevelAndCategory += loggingEvent.categoryName + ' - ';
  300. var output = timestampLevelAndCategory + loggingEvent.message;
  301. if (loggingEvent.exception) {
  302. output += '\n'
  303. output += timestampLevelAndCategory;
  304. if (loggingEvent.exception.stack) {
  305. output += loggingEvent.exception.stack;
  306. } else {
  307. output += loggingEvent.exception.name + ': '+loggingEvent.exception.message;
  308. }
  309. }
  310. return output;
  311. }
  312. /**
  313. * Taken from masylum's fork (https://github.com/masylum/log4js-node)
  314. */
  315. function colorize (str, style) {
  316. var styles = {
  317. //styles
  318. 'bold' : [1, 22],
  319. 'italic' : [3, 23],
  320. 'underline' : [4, 24],
  321. 'inverse' : [7, 27],
  322. //grayscale
  323. 'white' : [37, 39],
  324. 'grey' : [90, 39],
  325. 'black' : [90, 39],
  326. //colors
  327. 'blue' : [34, 39],
  328. 'cyan' : [36, 39],
  329. 'green' : [32, 39],
  330. 'magenta' : [35, 39],
  331. 'red' : [31, 39],
  332. 'yellow' : [33, 39]
  333. };
  334. return '\033[' + styles[style][0] + 'm' + str +
  335. '\033[' + styles[style][1] + 'm';
  336. }
  337. /**
  338. * colouredLayout - taken from masylum's fork.
  339. * same as basicLayout, but with colours.
  340. */
  341. function colouredLayout (loggingEvent) {
  342. var timestampLevelAndCategory = colorize('[' + loggingEvent.startTime.toFormattedString() + '] ', 'grey');
  343. timestampLevelAndCategory += colorize(
  344. '[' + loggingEvent.level.toString() + '] ', loggingEvent.level.colour
  345. );
  346. timestampLevelAndCategory += colorize(loggingEvent.categoryName + ' - ', 'grey');
  347. var output = timestampLevelAndCategory + loggingEvent.message;
  348. if (loggingEvent.exception) {
  349. output += '\n'
  350. output += timestampLevelAndCategory;
  351. if (loggingEvent.exception.stack) {
  352. output += loggingEvent.exception.stack;
  353. } else {
  354. output += loggingEvent.exception.name + ': '+loggingEvent.exception.message;
  355. }
  356. }
  357. return output;
  358. }
  359. function messagePassThroughLayout (loggingEvent) {
  360. return loggingEvent.message;
  361. }
  362. /**
  363. * PatternLayout
  364. * Takes a pattern string and returns a layout function.
  365. * @author Stephan Strittmatter
  366. */
  367. function patternLayout (pattern) {
  368. var TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n";
  369. var regex = /%(-?[0-9]+)?(\.?[0-9]+)?([cdmnpr%])(\{([^\}]+)\})?|([^%]+)/;
  370. pattern = pattern || patternLayout.TTCC_CONVERSION_PATTERN;
  371. return function(loggingEvent) {
  372. var formattedString = "";
  373. var result;
  374. var searchString = this.pattern;
  375. while ((result = regex.exec(searchString))) {
  376. var matchedString = result[0];
  377. var padding = result[1];
  378. var truncation = result[2];
  379. var conversionCharacter = result[3];
  380. var specifier = result[5];
  381. var text = result[6];
  382. // Check if the pattern matched was just normal text
  383. if (text) {
  384. formattedString += "" + text;
  385. } else {
  386. // Create a raw replacement string based on the conversion
  387. // character and specifier
  388. var replacement = "";
  389. switch(conversionCharacter) {
  390. case "c":
  391. var loggerName = loggingEvent.categoryName;
  392. if (specifier) {
  393. var precision = parseInt(specifier, 10);
  394. var loggerNameBits = loggingEvent.categoryName.split(".");
  395. if (precision >= loggerNameBits.length) {
  396. replacement = loggerName;
  397. } else {
  398. replacement = loggerNameBits.slice(loggerNameBits.length - precision).join(".");
  399. }
  400. } else {
  401. replacement = loggerName;
  402. }
  403. break;
  404. case "d":
  405. var dateFormat = Date.ISO8601_FORMAT;
  406. if (specifier) {
  407. dateFormat = specifier;
  408. // Pick up special cases
  409. if (dateFormat == "ISO8601") {
  410. dateFormat = Date.ISO8601_FORMAT;
  411. } else if (dateFormat == "ABSOLUTE") {
  412. dateFormat = Date.ABSOLUTETIME_FORMAT;
  413. } else if (dateFormat == "DATE") {
  414. dateFormat = Date.DATETIME_FORMAT;
  415. }
  416. }
  417. // Format the date
  418. replacement = loggingEvent.startTime.toFormattedString(dateFormat);
  419. break;
  420. case "m":
  421. replacement = loggingEvent.message;
  422. break;
  423. case "n":
  424. replacement = "\n";
  425. break;
  426. case "p":
  427. replacement = loggingEvent.level.toString();
  428. break;
  429. case "r":
  430. replacement = "" + loggingEvent.startTime.toLocaleTimeString();
  431. break;
  432. case "%":
  433. replacement = "%";
  434. break;
  435. default:
  436. replacement = matchedString;
  437. break;
  438. }
  439. // Format the replacement according to any padding or
  440. // truncation specified
  441. var len;
  442. // First, truncation
  443. if (truncation) {
  444. len = parseInt(truncation.substr(1), 10);
  445. replacement = replacement.substring(0, len);
  446. }
  447. // Next, padding
  448. if (padding) {
  449. if (padding.charAt(0) == "-") {
  450. len = parseInt(padding.substr(1), 10);
  451. // Right pad with spaces
  452. while (replacement.length < len) {
  453. replacement += " ";
  454. }
  455. } else {
  456. len = parseInt(padding, 10);
  457. // Left pad with spaces
  458. while (replacement.length < len) {
  459. replacement = " " + replacement;
  460. }
  461. }
  462. }
  463. formattedString += replacement;
  464. }
  465. searchString = searchString.substr(result.index + result[0].length);
  466. }
  467. return formattedString;
  468. };
  469. };
  470. module.exports = function (fileSystem, standardOutput, configPaths) {
  471. var fs = fileSystem || require('fs'),
  472. standardOutput = standardOutput || sys.puts,
  473. configPaths = configPaths || require.paths;
  474. function consoleAppender (layout) {
  475. layout = layout || colouredLayout;
  476. return function(loggingEvent) {
  477. standardOutput(layout(loggingEvent));
  478. };
  479. }
  480. /**
  481. * File Appender writing the logs to a text file. Supports rolling of logs by size.
  482. *
  483. * @param file file log messages will be written to
  484. * @param layout a function that takes a logevent and returns a string (defaults to basicLayout).
  485. * @param logSize - the maximum size (in bytes) for a log file, if not provided then logs won't be rotated.
  486. * @param numBackups - the number of log files to keep after logSize has been reached (default 5)
  487. * @param filePollInterval - the time in seconds between file size checks (default 30s)
  488. */
  489. function fileAppender (file, layout, logSize, numBackups, filePollInterval) {
  490. layout = layout || basicLayout;
  491. //syncs are generally bad, but we need
  492. //the file to be open before we start doing any writing.
  493. var logFile = fs.openSync(file, 'a', 0644);
  494. if (logSize > 0) {
  495. setupLogRolling(logFile, file, logSize, numBackups || 5, (filePollInterval * 1000) || 30000);
  496. }
  497. return function(loggingEvent) {
  498. fs.write(logFile, layout(loggingEvent)+'\n', null, "utf8");
  499. };
  500. }
  501. function setupLogRolling (logFile, filename, logSize, numBackups, filePollInterval) {
  502. fs.watchFile(filename,
  503. {
  504. persistent: false,
  505. interval: filePollInterval
  506. },
  507. function (curr, prev) {
  508. if (curr.size >= logSize) {
  509. rollThatLog(logFile, filename, numBackups);
  510. }
  511. }
  512. );
  513. }
  514. function rollThatLog (logFile, filename, numBackups) {
  515. //doing all of this fs stuff sync, because I don't want to lose any log events.
  516. //first close the current one.
  517. fs.closeSync(logFile);
  518. //roll the backups (rename file.n-1 to file.n, where n <= numBackups)
  519. for (var i=numBackups; i > 0; i--) {
  520. if (i > 1) {
  521. if (fileExists(filename + '.' + (i-1))) {
  522. fs.renameSync(filename+'.'+(i-1), filename+'.'+i);
  523. }
  524. } else {
  525. fs.renameSync(filename, filename+'.1');
  526. }
  527. }
  528. //open it up again
  529. logFile = fs.openSync(filename, 'a', 0644);
  530. }
  531. function fileExists (filename) {
  532. try {
  533. fs.statSync(filename);
  534. return true;
  535. } catch (e) {
  536. return false;
  537. }
  538. }
  539. function configure (configurationFileOrObject) {
  540. var config = configurationFileOrObject;
  541. if (typeof(config) === "string") {
  542. config = JSON.parse(fs.readFileSync(config, "utf8"));
  543. }
  544. if (config) {
  545. try {
  546. configureAppenders(config.appenders, fileAppender, consoleAppender);
  547. configureLevels(config.levels);
  548. } catch (e) {
  549. throw new Error("Problem reading log4js config " + sys.inspect(config) + ". Error was \"" + e.message + "\" ("+e.stack+")");
  550. }
  551. }
  552. }
  553. function findConfiguration() {
  554. //add current directory onto the list of configPaths
  555. var paths = ['.'].concat(configPaths);
  556. //add this module's directory to the end of the list, so that we pick up the default config
  557. paths.push(__dirname);
  558. var pathsWithConfig = paths.filter( function (pathToCheck) {
  559. try {
  560. fs.statSync(path.join(pathToCheck, "log4js.json"));
  561. return true;
  562. } catch (e) {
  563. return false;
  564. }
  565. });
  566. if (pathsWithConfig.length > 0) {
  567. return path.join(pathsWithConfig[0], 'log4js.json');
  568. }
  569. return undefined;
  570. }
  571. function replaceConsole(logger) {
  572. function replaceWith (fn) {
  573. return function() {
  574. fn.apply(logger, arguments);
  575. }
  576. }
  577. console.log = replaceWith(logger.info);
  578. console.debug = replaceWith(logger.debug);
  579. console.trace = replaceWith(logger.trace);
  580. console.info = replaceWith(logger.info);
  581. console.warn = replaceWith(logger.warn);
  582. console.error = replaceWith(logger.error);
  583. }
  584. //do we already have appenders?
  585. if (!appenders.count) {
  586. //set ourselves up if we can find a default log4js.json
  587. configure(findConfiguration());
  588. //replace console.log, etc with log4js versions
  589. replaceConsole(getLogger("console"));
  590. }
  591. return {
  592. getLogger: getLogger,
  593. getDefaultLogger: getDefaultLogger,
  594. addAppender: addAppender,
  595. clearAppenders: clearAppenders,
  596. configure: configure,
  597. levels: levels,
  598. consoleAppender: consoleAppender,
  599. fileAppender: fileAppender,
  600. logLevelFilter: logLevelFilter,
  601. basicLayout: basicLayout,
  602. messagePassThroughLayout: messagePassThroughLayout,
  603. patternLayout: patternLayout,
  604. colouredLayout: colouredLayout,
  605. coloredLayout: colouredLayout
  606. };
  607. }
  608. Date.ISO8601_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS";
  609. Date.ISO8601_WITH_TZ_OFFSET_FORMAT = "yyyy-MM-ddThh:mm:ssO";
  610. Date.DATETIME_FORMAT = "dd MMM YYYY hh:mm:ss.SSS";
  611. Date.ABSOLUTETIME_FORMAT = "hh:mm:ss.SSS";
  612. Date.prototype.toFormattedString = function(format) {
  613. format = format || Date.ISO8601_FORMAT;
  614. var vDay = addZero(this.getDate());
  615. var vMonth = addZero(this.getMonth()+1);
  616. var vYearLong = addZero(this.getFullYear());
  617. var vYearShort = addZero(this.getFullYear().toString().substring(3,4));
  618. var vYear = (format.indexOf("yyyy") > -1 ? vYearLong : vYearShort);
  619. var vHour = addZero(this.getHours());
  620. var vMinute = addZero(this.getMinutes());
  621. var vSecond = addZero(this.getSeconds());
  622. var vMillisecond = padWithZeros(this.getMilliseconds(), 3);
  623. var vTimeZone = offset(this);
  624. var formatted = format
  625. .replace(/dd/g, vDay)
  626. .replace(/MM/g, vMonth)
  627. .replace(/y{1,4}/g, vYear)
  628. .replace(/hh/g, vHour)
  629. .replace(/mm/g, vMinute)
  630. .replace(/ss/g, vSecond)
  631. .replace(/SSS/g, vMillisecond)
  632. .replace(/O/g, vTimeZone);
  633. return formatted;
  634. function padWithZeros(vNumber, width) {
  635. var numAsString = vNumber + "";
  636. while (numAsString.length < width) {
  637. numAsString = "0" + numAsString;
  638. }
  639. return numAsString;
  640. }
  641. function addZero(vNumber) {
  642. return padWithZeros(vNumber, 2);
  643. }
  644. /**
  645. * Formats the TimeOffest
  646. * Thanks to http://www.svendtofte.com/code/date_format/
  647. * @private
  648. */
  649. function offset(date) {
  650. // Difference to Greenwich time (GMT) in hours
  651. var os = Math.abs(date.getTimezoneOffset());
  652. var h = String(Math.floor(os/60));
  653. var m = String(os%60);
  654. h.length == 1? h = "0"+h:1;
  655. m.length == 1? m = "0"+m:1;
  656. return date.getTimezoneOffset() < 0 ? "+"+h+m : "-"+h+m;
  657. }
  658. };