diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8d785cf63..b5e1ad300d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## v0.10.0 (13 Oct 2014)
+
+* Additions:
+ - `format`
+ - `startOfYear`
+
## v0.9.0 (10 Oct 2014)
* Additions:
diff --git a/package.json b/package.json
index 20f09c71dc..d7d0452a97 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "date-fns",
- "version": "0.9.0",
+ "version": "0.10.0",
"author": "Sasha Koss ",
"description": "Date helpers",
"repository": "https://github.com/kossnocorp/date-fns",
diff --git a/src/__tests__/format_test.js b/src/__tests__/format_test.js
new file mode 100644
index 0000000000..e00650e7b7
--- /dev/null
+++ b/src/__tests__/format_test.js
@@ -0,0 +1,125 @@
+var format = require('../format');
+
+describe('format', function() {
+ beforeEach(function () {
+ this._date = new Date(1986, 3, 4, 10, 32, 0, 900);
+ });
+
+ it('simple YY', function() {
+ var b = new Date(2009, 1, 14, 15, 25, 50, 125);
+ expect(format(b, 'YY')).to.equal('09');
+ });
+
+ it('accepts string as a date', function() {
+ expect(format('2014-04-04', 'YYYY-MM-DD')).to.be.equal('2014-04-04');
+ });
+
+ it('return default ISO string format if format is unknown', function() {
+ expect(format(this._date)).to.be.equal('1986-04-04T10:32:00.900Z');
+ });
+
+ describe('format escape brackets', function() {
+ it('should ignore escaped chars that in [] brackets', function() {
+ var result = format(this._date, '[not a date] MM');
+ expect(result).to.be.equal('not a date 04');
+ });
+ });
+
+ describe('ordinal', function() {
+ it('should return 1st', function() {
+ var date = format(this._date, 'Do of t[h][e] Mo in YYYY');
+ expect(date).to.be.equal('4th of the 4th in 1986');
+ });
+ });
+
+ describe('Months', function() {
+ it('return months names', function() {
+ var date = format(this._date, 'MMM MMMM');
+ expect(date).to.equal('Apr April');
+ });
+ it('return months names reverse parse', function() {
+ var date = format(this._date, 'MMMM MMM');
+ expect(date).to.equal('April Apr');
+ });
+ it('all month variants', function() {
+ var date = format(this._date, 'M Mo MM MMM MMMM');
+ expect(date).to.equal('4 4th 04 Apr April');
+ });
+ });
+
+ describe('formatting day', function() {
+ describe('with DDD', function() {
+ context('for first day of a year', function() {
+ it('returns correct day number', function() {
+ var result = format(new Date(1992, 0, 1, 0, 0, 0, 0), 'DDD');
+ expect(result).to.be.equal('1');
+ });
+ });
+
+ context('for last day of a common year', function() {
+ it('returns correct day number', function() {
+ var lastDay = format(new Date(1986, 11, 31, 23, 59, 59, 999), 'DDD');
+ expect(lastDay).to.be.equal('365');
+ });
+ });
+
+ context('for last day of a leap year', function() {
+ it('returns correct day number', function() {
+ var result = format(new Date(1992, 11, 31, 23, 59, 59, 999), 'DDD');
+ expect(result).to.be.equal('366');
+ });
+ });
+ });
+
+ it('return ordinal day of the year', function() {
+ var firstDay = format(new Date(1992, 0, 1, 0, 0, 0, 0), 'DDDo');
+ expect(firstDay).to.be.equal('1st');
+ });
+
+ it('return zero filled day of year', function() {
+ var firstDay = format(new Date(1992, 0, 1, 0, 0, 0, 0), 'DDDD');
+ expect(firstDay).to.be.equal('001');
+ });
+ });
+
+ describe('Quartal', function() {
+ it('right quartal', function() {
+ var result = [];
+ for (var i = 0; i != 12; i++){
+ result.push(format(new Date(1986, i, 1), 'Q'));
+ }
+ expect(result).to.deep.equal(
+ ['1','1','1', '2', '2', '2', '3', '3', '3', '4', '4', '4']
+ );
+ });
+ });
+
+ describe('day of week', function() {
+ it('display', function() {
+ var result = format(this._date, 'd do dd ddd dddd');
+ expect(result).to.be.equal('5 5th Fr Fri Friday');
+ });
+
+ it('ISO', function() {
+ expect(format(this._date, 'E')).to.be.equal('6');
+ });
+
+ it('parses ok for different variants', function() {
+ var firstDay = format(this._date, 'dddd ddd d do [d] do dd ddd dddd');
+ expect(firstDay).to.be.equal('Friday Fri 5 5th d 5th Fr Fri Friday');
+ });
+ });
+
+ describe('hours', function() {
+ it('am/pm', function() {
+ expect(format(this._date, 'hh:mm a')).to.be.equal('10:32 am');
+ });
+ });
+
+ describe('seconds', function() {
+ it('show', function() {
+ expect(format(this._date, 's ss')).to.be.equal('0 00');
+ });
+ });
+});
+
diff --git a/src/__tests__/start_of_year_test.js b/src/__tests__/start_of_year_test.js
new file mode 100644
index 0000000000..0c603caa57
--- /dev/null
+++ b/src/__tests__/start_of_year_test.js
@@ -0,0 +1,26 @@
+var startOfYear = require('../start_of_year');
+
+describe('startOfYear', function() {
+ it('returns date with time setted to 00:00:00', function() {
+ var date = new Date(2014, 8, 2, 11, 55, 00);
+ var result = startOfYear(date);
+ expect(result).to.be.eql(
+ new Date(2014, 0, 1, 0, 0, 0, 0)
+ );
+ });
+
+ it('accepts string', function() {
+ var date = new Date(2014, 8, 2, 11, 55, 00);
+ var result = startOfYear(date.toISOString());
+ expect(result).to.be.eql(
+ new Date(2014, 0, 1, 0, 0, 0, 0)
+ );
+ });
+
+ it('do not mutates original date', function() {
+ var date = new Date('2014-09-02T11:55:00');
+ startOfYear(date);
+ expect(date).to.be.eql(new Date('2014-09-02T11:55:00'));
+ });
+});
+
diff --git a/src/format.js b/src/format.js
new file mode 100644
index 0000000000..fdee2af3d8
--- /dev/null
+++ b/src/format.js
@@ -0,0 +1,184 @@
+var startOfDay = require('./start_of_day');
+var startOfYear = require('./start_of_year');
+
+var NUMBER_OF_MS_IN_DAY = 864e5;
+
+/**
+ * Returns formatted date string in a given format
+ * @param {date|string} date
+ * @param {string} format
+ * @returns {string}
+ */
+var format = function(date, format) {
+ date = date instanceof Date ? date : new Date(date);
+
+ if (!format) {
+ format = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
+ };
+
+ var formatFunction = makeFormatFunction(format);
+ return formatFunction(date);
+};
+
+var formats = {
+ 'M': function() {
+ return this.getMonth() + 1;
+ },
+ 'MM': function() {
+ return leftZeroFill(this.getMonth() + 1, 2);
+ },
+ 'MMM': function() {
+ return locale.monthsShort[this.getMonth()];
+ },
+ 'MMMM': function() {
+ return locale.months[this.getMonth()];
+ },
+ 'Q': function() {
+ return Math.ceil((this.getMonth() + 1) / 3);
+ },
+ 'D': function() {
+ return this.getDate();
+ },
+ 'DD': function() {
+ return leftZeroFill(this.getDate(), 2);
+ },
+ 'DDD': function() {
+ var diffWithStartOfYear =
+ startOfDay(this).getTime() - startOfYear(this).getTime();
+ return diffWithStartOfYear / NUMBER_OF_MS_IN_DAY + 1;
+ },
+ 'DDDD': function() {
+ return leftZeroFill(formats['DDD'].apply(this), 3);
+ },
+ 'd': function() {
+ return this.getDay();
+ },
+ 'dd': function() {
+ return locale.dayNamesMin[this.getDay()];
+ },
+ 'ddd': function() {
+ return locale.dayNamesShort[this.getDay()];
+ },
+ 'dddd': function() {
+ return locale.dayNames[this.getDay()];
+ },
+ 'E': function() {
+ return this.getDay() + 1;
+ },
+ 'YY': function() {
+ return String(this.getFullYear()).substr(2);
+ },
+ 'YYYY': function() {
+ return this.getFullYear()
+ },
+ 'A': function() {
+ return (this.getHours() / 12) >= 1 ? 'PM' : 'AM';
+ },
+ 'a': function() {
+ return (this.getHours() / 12) >= 1 ? 'pm' : 'am';
+ },
+ 'H': function() {
+ return this.getHours();
+ },
+ 'HH': function() {
+ return leftZeroFill(this.getHours(), 2);
+ },
+ 'h': function() {
+ return this.getHours() % 12;
+ },
+ 'hh': function() {
+ return leftZeroFill(this.getHours() % 12, 2);
+ },
+ 'm': function() {
+ return this.getMinutes();
+ },
+ 'mm': function() {
+ return leftZeroFill(this.getMinutes());
+ },
+ 's': function() {
+ return this.getSeconds();
+ },
+ 'ss': function() {
+ return leftZeroFill(this.getSeconds(), 2);
+ },
+ 'S': function() {
+ return this.getMilliseconds();
+ },
+ 'SS': function() {
+ return leftZeroFill(this.getMilliseconds(), 2);
+ },
+ 'SSS': function() {
+ return leftZeroFill(this.getMilliseconds(), 3);
+ }
+};
+
+var ordinalFunctions = ['M', 'D', 'DDD', 'd'];
+ordinalFunctions.forEach(function(functionName){
+ formats[functionName + 'o'] = function() {
+ return locale.ordinal(formats[functionName].apply(this));
+ };
+});
+
+var formattingTokens = Object.keys(formats).sort().reverse();
+var formattingTokensRegexp = new RegExp(
+ '(\\[[^\\[]*\\])|(\\\\)?' + '(' + formattingTokens.join('|') + '|.)', 'g'
+);
+
+var makeFormatFunction = function(format) {
+ var array = format.match(formattingTokensRegexp), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formats[array[i]]) {
+ array[i] = formats[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function(mom) {
+ var output = '';
+ for (i = 0; i < length; i++) {
+ if (array[i] instanceof Function) {
+ output += array[i].call(mom, format);
+ } else {
+ output += array[i];
+ }
+ }
+ return output;
+ };
+};
+
+var removeFormattingTokens = function(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, '');
+ }
+ return input.replace(/\\/g, '');
+};
+
+var leftZeroFill = function(number, targetLength) {
+ var output = '' + Math.abs(number);
+
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return output;
+};
+
+var locale = {
+ ordinal: function(number) {
+ var b = number % 10,
+ output = (+(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ },
+ months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
+};
+
+module.exports = format;
+
diff --git a/src/start_of_year.js b/src/start_of_year.js
new file mode 100644
index 0000000000..ede03b8ffd
--- /dev/null
+++ b/src/start_of_year.js
@@ -0,0 +1,13 @@
+/**
+ * Returns start of a year for given date. Date will be in local timezone.
+ * @param {date|string} dirtyDate
+ * @returns {date}
+ */
+var startOfYear = function(dirtyDate) {
+ var cleanDate = new Date(dirtyDate);
+ var date = new Date(cleanDate.getFullYear(), 0, 1, 0, 0, 0, 0);
+ return date;
+};
+
+module.exports = startOfYear;
+