Source: columns/DateTimeColumn.js

import { AbstractColumn } from './AbstractColumn.js';

/**
 * Formats a Date object using a Python-style strftime specifier string (%d, %m, %Y, %H, %M, %S).
 *
 * @param {Date} dateObj - The UTC Date object to format.
 * @param {string} formatStr - Format pattern (e.g. "%d%m%Y%H%M%S").
 * @returns {string} Formatted date/time string.
 */
export function formatPythonDate(dateObj, formatStr) {
  const d = String(dateObj.getUTCDate()).padStart(2, '0');
  const m = String(dateObj.getUTCMonth() + 1).padStart(2, '0');
  const Y = String(dateObj.getUTCFullYear()).padStart(4, '0');
  const H = String(dateObj.getUTCHours()).padStart(2, '0');
  const M = String(dateObj.getUTCMinutes()).padStart(2, '0');
  const S = String(dateObj.getUTCSeconds()).padStart(2, '0');

  return formatStr
    .replace(/%d/g, d)
    .replace(/%m/g, m)
    .replace(/%Y/g, Y)
    .replace(/%H/g, H)
    .replace(/%M/g, M)
    .replace(/%S/g, S);
}

/**
 * Parses a string formatted according to a Python-style strptime format pattern into a UTC Date object.
 *
 * @param {string} str - Raw date/time string.
 * @param {string} formatStr - Format pattern (e.g. "%d%m%Y%H%M").
 * @returns {Date|null} Parsed UTC Date object or null if parsing fails/invalid date.
 */
export function parsePythonDate(str, formatStr) {
  let regexStr = '^';
  const groupOrder = [];
  let i = 0;
  while (i < formatStr.length) {
    if (formatStr[i] === '%') {
      const spec = formatStr[i + 1];
      if (spec === 'd' || spec === 'm' || spec === 'H' || spec === 'M' || spec === 'S') {
        regexStr += '(\\d{2})';
        groupOrder.push(spec);
      } else if (spec === 'Y') {
        regexStr += '(\\d{4})';
        groupOrder.push('Y');
      } else {
        regexStr += '\\' + spec;
      }
      i += 2;
    } else {
      regexStr += formatStr[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      i++;
    }
  }
  regexStr += '$';

  const match = str.match(new RegExp(regexStr));
  if (!match) return null;

  let year = 2000;
  let month = 1;
  let day = 1;
  let hour = 0;
  let min = 0;
  let sec = 0;

  for (let g = 0; g < groupOrder.length; g++) {
    const val = parseInt(match[g + 1], 10);
    const spec = groupOrder[g];
    if (spec === 'Y') year = val;
    if (spec === 'm') month = val;
    if (spec === 'd') day = val;
    if (spec === 'H') hour = val;
    if (spec === 'M') min = val;
    if (spec === 'S') sec = val;
  }

  if (month < 1 || month > 12) return null;
  if (day < 1 || day > 31) return null;
  if (hour < 0 || hour > 23) return null;
  if (min < 0 || min > 59) return null;
  if (sec < 0 || sec > 59) return null;

  const res = new Date(Date.UTC(year, month - 1, day, hour, min, sec));
  if (isNaN(res.getTime())) return null;
  return res;
}

/**
 * DateTime column supporting custom date-time formatting (%d, %m, %Y, %H, %M, %S) and null/zero-padding.
 * Parses raw fixed-width date-time strings to UTC Date objects and vice versa.
 *
 * @extends AbstractColumn
 * @example
 * const col = new DateTimeColumn('created_at', '%d%m%Y%H%M', 'Creation Timestamp');
 * col.toValue('210720261640'); // Date object (UTC: 2026-07-21T16:40:00.000Z)
 */
export class DateTimeColumn extends AbstractColumn {
  /**
   * Creates an instance of DateTimeColumn.
   *
   * @param {string} name - Column name.
   * @param {string} [format="%d%m%Y%H%M"] - Date format string using Python strftime patterns.
   * @param {string|null} [description=null] - Column description.
   * @param {number} [requiredFormatNumElements=5] - Number of format specifiers required.
   * @throws {TypeError} If name or format is not a string.
   * @throws {Error} If name or format is blank, or format specifier count does not match requirement.
   */
  constructor(name, format = '%d%m%Y%H%M', description = null, requiredFormatNumElements = 5) {
    if (typeof name !== 'string') {
      throw new TypeError('O campo name deve ser uma string');
    }
    if (!name || !name.trim()) {
      throw new Error('O campo column_name deve ser uma string válida e não branca');
    }
    if (typeof format !== 'string') {
      throw new TypeError(`O argumento '_format' do campo '${name}' deve ser uma string`);
    }
    if (!format || !format.trim()) {
      throw new Error(`O argumento '_format' do campo '${name}' deve ser uma string válida e não branca`);
    }

    const matches = format.match(/%[a-zA-Z]/g) || [];
    if (matches.length !== requiredFormatNumElements) {
      throw new Error(`O argumento '_format' (${format}) do campo '${name}' deve ter um formato de data/hora válido`);
    }

    const sampleDate = new Date(Date.UTC(2001, 11, 31, 13, 59, 0));
    const size = formatPythonDate(sampleDate, format).length;

    super(name, size, description);

    /**
     * Date format pattern.
     * @type {string}
     */
    this.format = format;

    /**
     * Expected specifier count.
     * @type {number}
     */
    this.requiredFormatNumElements = requiredFormatNumElements;

    /**
     * Type descriptor for error assertions.
     * @type {string}
     */
    this.assertionType = 'datetime';
  }

  /**
   * Converts raw string slice from file into a UTC Date object.
   *
   * @param {string} slice - Raw fixed-width date-time string.
   * @returns {Date|null} Parsed Date object or null if zero-filled string.
   * @throws {Error} If slice does not match date-time format.
   */
  toValue(slice) {
    super.toValue(slice);
    if (slice === '0'.repeat(this.size)) {
      return null;
    }
    const parsed = parsePythonDate(slice, this.format);
    if (!parsed) {
      throw new Error(`O valor '${slice}' do campo '${this.name}' é inválido para o formato '${this.format}'`);
    }
    return parsed;
  }

  /**
   * Serializes a Date object into a formatted fixed-width string.
   *
   * @param {Date|null} value - Date object to format. Null/undefined serializes to zero string.
   * @returns {string} Formatted fixed-width string.
   * @throws {TypeError} If value is not a valid Date instance.
   */
  toStr(value) {
    if (value === null || value === undefined) {
      return '0'.repeat(this.size);
    }
    if (!(value instanceof Date) || isNaN(value.getTime())) {
      throw new TypeError(`O campo '${this.name}' só aceita '${this.assertionType}' ou 'None'`);
    }
    const formatted = formatPythonDate(value, this.format);
    return this.validateToStrSize(formatted);
  }
}