valor-software/ng2-bootstrap

View on GitHub
src/chronos/create/from-string.ts

Summary

Maintainability
C
1 day
Test Coverage
// tslint:disable-next-line
import { defaultLocaleMonthsShort, defaultLocaleWeekdaysShort } from '../locale/locale.class';
import { DateArray } from '../types';
import { DateParsingConfig } from './parsing.types';
import { isString } from '../utils/type-checks';
import { configFromStringAndFormat } from './from-string-and-format';
import { createUTCDate } from './date-from-array';
import { createInvalid, markInvalid } from './valid';
import { getParsingFlags } from './parsing-flags';

// iso 8601 regex
// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
// tslint:disable-next-line
const extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
// tslint:disable-next-line
const basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;

const tzRegex = /Z|[+-]\d\d(?::?\d\d)?/;

const isoDates: [[string, RegExp, boolean]] = [
  ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/, true],
  ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/, true],
  ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/, true],
  ['GGGG-[W]WW', /\d{4}-W\d\d/, false],
  ['YYYY-DDD', /\d{4}-\d{3}/, true],
  ['YYYY-MM', /\d{4}-\d\d/, false],
  ['YYYYYYMMDD', /[+-]\d{10}/, true],
  ['YYYYMMDD', /\d{8}/, true],
  // YYYYMM is NOT allowed by the standard
  ['GGGG[W]WWE', /\d{4}W\d{3}/, true],
  ['GGGG[W]WW', /\d{4}W\d{2}/, false],
  ['YYYYDDD', /\d{7}/, true]
];

// iso time formats and regexes
const isoTimes: [[string, RegExp]] = [
  ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/],
  ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/],
  ['HH:mm:ss', /\d\d:\d\d:\d\d/],
  ['HH:mm', /\d\d:\d\d/],
  ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/],
  ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/],
  ['HHmmss', /\d\d\d\d\d\d/],
  ['HHmm', /\d\d\d\d/],
  ['HH', /\d\d/]
];

const aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;

const obsOffsets: { [key: string]: number } = {
  UT: 0,
  GMT: 0,
  EDT: -4 * 60,
  EST: -5 * 60,
  CDT: -5 * 60,
  CST: -6 * 60,
  MDT: -6 * 60,
  MST: -7 * 60,
  PDT: -7 * 60,
  PST: -8 * 60
};

// RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3
// tslint:disable-next-line
const rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;

// date from iso format
export function configFromISO(config: DateParsingConfig): DateParsingConfig {
  if (!isString(config._i)) {
    return config;
  }

  const input = config._i;
  const match = extendedIsoRegex.exec(input) || basicIsoRegex.exec(input);


  let allowTime;
  let dateFormat;
  let timeFormat;
  let tzFormat;

  if (!match) {
    config._isValid = false;

    return config;
  }

  // getParsingFlags(config).iso = true;
  let i;
  let l;
  for (i = 0, l = isoDates.length; i < l; i++) {
    if (isoDates[i][1].exec(match[1])) {
      dateFormat = isoDates[i][0];
      allowTime = isoDates[i][2] !== false;
      break;
    }
  }

  if (dateFormat == null) {
    config._isValid = false;

    return config;
  }

  if (match[3]) {
    for (i = 0, l = isoTimes.length; i < l; i++) {
      if (isoTimes[i][1].exec(match[3])) {
        // match[2] should be 'T' or space
        timeFormat = (match[2] || ' ') + isoTimes[i][0];
        break;
      }
    }

    if (timeFormat == null) {
      config._isValid = false;

      return config;
    }

  }
  if (!allowTime && timeFormat != null) {
    config._isValid = false;

    return config;
  }

  if (match[4]) {
    if (tzRegex.exec(match[4])) {
      tzFormat = 'Z';
    } else {
      config._isValid = false;

      return config;
    }
  }

  config._f = dateFormat + (timeFormat || '') + (tzFormat || '');

  return configFromStringAndFormat(config);
}

// tslint:disable-next-line
function extractFromRFC2822Strings(yearStr: string, monthStr: string, dayStr: string, hourStr: string, minuteStr: string, secondStr: string): DateArray {
  const result = [
    untruncateYear(yearStr),
    defaultLocaleMonthsShort.indexOf(monthStr),
    parseInt(dayStr, 10),
    parseInt(hourStr, 10),
    parseInt(minuteStr, 10)
  ];

  if (secondStr) {
    result.push(parseInt(secondStr, 10));
  }

  return result;
}

function untruncateYear(yearStr: string): number {
  const year = parseInt(yearStr, 10);

  return year <= 49 ? year + 2000 : year;
}

function preprocessRFC2822(str: string): string {
  // Remove comments and folding whitespace and replace multiple-spaces with a single space
  return str
    .replace(/\([^)]*\)|[\n\t]/g, ' ')
    .replace(/(\s\s+)/g, ' ').trim();
}

function checkWeekday(weekdayStr: string, parsedInput: DateArray, config: DateParsingConfig): boolean {
  if (weekdayStr) {
    // TODO: Replace the vanilla JS Date object with an indepentent day-of-week check.
    const weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr);
    const weekdayActual = new Date(parsedInput[0], parsedInput[1], parsedInput[2]).getDay();
    if (weekdayProvided !== weekdayActual) {
      getParsingFlags(config).weekdayMismatch = true;
      config._isValid = false;

      return false;
    }
  }

  return true;
}

function calculateOffset(obsOffset: string, militaryOffset: string, numOffset: string) {
  if (obsOffset) {
    return obsOffsets[obsOffset];
  } else if (militaryOffset) {
    // the only allowed military tz is Z
    return 0;
  } else {
    const hm = parseInt(numOffset, 10);
    const m = hm % 100;
    const h = (hm - m) / 100;

    return h * 60 + m;
  }
}

// date and time from ref 2822 format
export function configFromRFC2822(config: DateParsingConfig): DateParsingConfig {
  if (!isString(config._i)) {
    return config;
  }

  const match = rfc2822.exec(preprocessRFC2822(config._i));

  if (!match) {
    return markInvalid(config);
  }

  const parsedArray = extractFromRFC2822Strings(match[4], match[3], match[2], match[5], match[6], match[7]);
  if (!checkWeekday(match[1], parsedArray, config)) {
    return config;
  }

  config._a = parsedArray;
  config._tzm = calculateOffset(match[8], match[9], match[10]);

  config._d = createUTCDate.apply(null, config._a);
  config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);

  getParsingFlags(config).rfc2822 = true;

  return config;
}

// date from iso format or fallback
export function configFromString(config: DateParsingConfig): DateParsingConfig {
  if (!isString(config._i)) {
    return config;
  }

  const matched = aspNetJsonRegex.exec(config._i);

  if (matched !== null) {
    config._d = new Date(+matched[1]);

    return config;
  }

  // todo: update logic processing
  // isISO -> configFromISO
  // isRFC -> configFromRFC

  configFromISO(config);
  if (config._isValid === false) {
    delete config._isValid;
  } else {
    return config;
  }

  configFromRFC2822(config);
  if (config._isValid === false) {
    delete config._isValid;
  } else {
    return config;
  }

  // Final attempt, use Input Fallback
  // hooks.createFromInputFallback(config);
  return createInvalid(config);
}

// hooks.createFromInputFallback = deprecate(
//     'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' +
//     'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' +
//     'discouraged and will be removed in an upcoming major release. Please refer to ' +
//     'http://momentjs.com/guides/#/warnings/js-date/ for more info.',
//     function (config) {
//         config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
//     }
// );