define(['../array/some'], function (some) {

    var datePatterns = [
        /^([0-9]{4})$/,                        // YYYY
        /^([0-9]{4})-([0-9]{2})$/,             // YYYY-MM (YYYYMM not allowed)
        /^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/ // YYYY-MM-DD or YYYYMMDD
    ];
    var ORD_DATE = /^([0-9]{4})-?([0-9]{3})$/; // YYYY-DDD

    var timePatterns = [
        /^([0-9]{2}(?:\.[0-9]*)?)$/,                      // HH.hh
        /^([0-9]{2}):?([0-9]{2}(?:\.[0-9]*)?)$/,          // HH:MM.mm
        /^([0-9]{2}):?([0-9]{2}):?([0-9]{2}(\.[0-9]*)?)$/ // HH:MM:SS.ss
    ];

    var DATE_TIME = /^(.+)T(.+)$/;
    var TIME_ZONE = /^(.+)([+\-])([0-9]{2}):?([0-9]{2})$/;

    function matchAll(str, patterns) {
        var match;
        var found = some(patterns, function(pattern) {
            return !!(match = pattern.exec(str));
        });

        return found ? match : null;
    }

    function getDate(year, month, day) {
        var date = new Date(Date.UTC(year, month, day));

        // Explicitly set year to avoid Date.UTC making dates < 100 relative to
        // 1900
        date.setUTCFullYear(year);

        var valid =
            date.getUTCFullYear() === year &&
            date.getUTCMonth() === month &&
            date.getUTCDate() === day;
        return valid ? +date : NaN;
    }

    function parseOrdinalDate(str) {
        var match = ORD_DATE.exec(str);
        if (match ) {
            var year = +match[1],
                day = +match[2],
                date = new Date(Date.UTC(year, 0, day));

            if (date.getUTCFullYear() === year) {
                return +date;
            }
        }

        return NaN;
    }

    function parseDate(str) {
        var match, year, month, day;

        match = matchAll(str, datePatterns);
        if (match === null) {
            // Ordinal dates are verified differently.
            return parseOrdinalDate(str);
        }

        year = (match[1] === void 0) ? 0 : +match[1];
        month = (match[2] === void 0) ? 0 : +match[2] - 1;
        day = (match[3] === void 0) ? 1 : +match[3];

        return getDate(year, month, day);
    }

    function getTime(hr, min, sec) {
        var valid =
            (hr < 24 && hr >= 0 &&
             min < 60 && min >= 0 &&
             sec < 60 && min >= 0) ||
            (hr === 24 && min === 0 && sec === 0);
        if (!valid) {
            return NaN;
        }

        return ((hr * 60 + min) * 60 + sec) * 1000;
    }

    function parseOffset(str) {
        var match;
        if (str.charAt(str.length - 1) === 'Z') {
            str = str.substring(0, str.length - 1);
        } else {
            match = TIME_ZONE.exec(str);
            if (match) {
                var hours = +match[3],
                    minutes = (match[4] === void 0) ? 0 : +match[4],
                    offset = getTime(hours, minutes, 0);

                if (match[2] === '-') {
                    offset *= -1;
                }

                return { offset: offset, time: match[1] };
            }
        }

        // No time zone specified, assume UTC
        return { offset: 0, time: str };
    }

    function parseTime(str) {
        var match;
        var offset = parseOffset(str);

        str = offset.time;
        offset = offset.offset;
        if (isNaN(offset)) {
            return NaN;
        }

        match = matchAll(str, timePatterns);
        if (match === null) {
            return NaN;
        }

        var hours = (match[1] === void 0) ? 0 : +match[1],
            minutes = (match[2] === void 0) ? 0 : +match[2],
            seconds = (match[3] === void 0) ? 0 : +match[3];

        return getTime(hours, minutes, seconds) - offset;
    }

    /**
     * Parse an ISO8601 formatted date string, and return a Date object.
     */
    function parseISO8601(str){
        var match = DATE_TIME.exec(str);
        if (!match) {
            // No time specified
            return parseDate(str);
        }

        return parseDate(match[1]) + parseTime(match[2]);
    }

    return parseISO8601;

});
