typescript

Parsing strings as Date Ranges in TypeScript


Trying to refactor/optimise an existing method parsing the date range or a date:

const parseDateRange = (range: string): Date | null => {
if (!range?.length) {
            return null;
}
    // Handle "MM/YYYY" format
    if (/^\d{1,2}\/\d{4}$/.test(range)) {
            const [month, year] = range.split('/').map(Number);
            if (!isNaN(month) && !isNaN(year)) {
              return new Date(year, month - 1);
            }
          }
      
          // Handle "MM/DD/YYYY-MM/DD/YYYY" format
          const startDateString = range.slice(0, range.indexOf('-')).trim();
          const startDate = new Date(startDateString);
          if (!isNaN(startDate.getTime())) {
            return startDate;
     }
     return null;
};

I can probably use something like this type here:

type Split<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, U]:
[S];

Would appreciate the recommendation how I would utilize this or similar to parse the delimited date range type here.


Solution

  • I am sure you can use different libraries for this such Moment.js and date-fns,

    But I am just going to assume you need it for some specific cases(like you dont want an external lib). In that case you can go about this using this code I have provided below.

    type Split<S extends string, D extends string> = string extends S
      ? string[]
      : S extends ""
      ? []
      : S extends `${infer T}${D}${infer U}`
      ? [T, U]
      : [S];
    
    const parseDateRange = (range: string): string | null => {
      if (!range?.length) {
        return null;
      }
    
      // Handle "MM/YYYY" format
      const mmYYYYMatch = /^(\d{1,2})\/(\d{4})$/.exec(range);
      if (mmYYYYMatch) {
        const [_, month, year] = mmYYYYMatch.map(Number);
        if (month && year) {
          let new_date = new Date(year, month - 1);
          return new_date.toUTCString();
        }
      }
    
      // Handle "MM/DD/YYYY-MM/DD/YYYY" format
      const [start, end] = range.split("-").map((date) => date.trim());
      const startDate = new Date(start);
      const endDate = new Date(end);
    
      if (!isNaN(startDate.getTime())) {
        return startDate.toUTCString(); // Optionally, handle `endDate` if needed.
      }
    
      return null;
    };
    
    // Example usage
    console.log(parseDateRange("08/2024")); // Expected output: Date object for August 2024
    console.log(parseDateRange("08/12/2024-08/13/2024")); // Expected output: Date object for August 12, 2024
    console.log(parseDateRange("Invalid input")); // Expected output: null
    
    // convert from Z U T C to local time
    const date = new Date("2024-08-12T00:00:00Z");
    console.log(date.toLocaleString()); // Expected output: Local time for August 12, 2024
    
    

    Results

    Wed, 31 Jul 2024 23:00:00 GMT
    Sun, 11 Aug 2024 23:00:00 GMT
    null
    8/12/2024, 1:00:00 AM
    

    A request by OP on how to use Spilt type if I wanted to split a date

    Here is an example :

    
    type Split<S extends string, D extends string> = string extends S
      ? string[]
      : S extends ""
      ? []
      : S extends `${infer T}${D}${infer U}`
      ? [T, U]
      : [S];
    
    // a type enforcer which expects a format of the date string
    type DateParts = Split<'MM/DD/YYYY', '/'>; // the result would be something like this ['MM', 'DD', 'YYYY']
    
    // func that uses the `Split` type to validate input structures.
    function parseDateParts<T extends string>(formatDateToString: T): Split<T, '/'> {
      return formatDateToString.split('/') as Split<T, '/'>;
    }
    
    // how ts would infer from it
    const parts = parseDateParts('08/12/2024'); // TypeScript infers ['08', '12', '2024']
    

    Alternatively

    You can use it in this way

    
    // the spilt type provided
    type Split<S extends string, D extends string> = string extends S
      ? string[]
      : S extends ""
      ? []
      : S extends `${infer T}${D}${infer U}`
      ? [T, ...Split<U, D>]
      : [S];
    
    // trying to mimic date formats ... 
    type MMYYYY = `${number}/${number}`;
    
    //  MM/DD/YYYY-MM/DD/YYYY format
    type DateRange = `${number}/${number}/${number}-${number}/${number}/${number}`;
    
    // refactor func to use utility types
    const parseDateRange = (range: string): Date | null => {
      if (!range?.length) {
        return null;
      }
    
      // match the MM/YYYY format
      if (/^\d{1,2}\/\d{4}$/.test(range)) {
        // Use Split to ensure correct type handling
        const [month, year] = range.split("/") as Split<MMYYYY, "/">;
        const date = new Date(Number(year), Number(month) - 1);
        return isNaN(date.getTime()) ? null : date;
      }
      return null;
    };