airbnb/superset

View on GitHub
superset-frontend/packages/superset-ui-core/src/time-comparison/getTimeOffset.ts

Summary

Maintainability
F
4 days
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { isEmpty } from 'lodash';
import { ensureIsArray } from '../utils';
import { customTimeRangeDecode } from './customTimeRangeDecode';

const DAY_IN_MS = 24 * 60 * 60 * 1000;
export const parseDttmToDate = (
  dttm: string,
  isEndDate = false,
  computingShifts = false,
) => {
  const now = new Date();
  if (
    dttm === 'now' ||
    dttm === 'today' ||
    dttm === 'No filter' ||
    dttm === ''
  ) {
    return now;
  }

  if (computingShifts) {
    now.setHours(-now.getTimezoneOffset() / 60, 0, 0, 0);
  } else {
    now.setHours(0, 0, 0, 0);
  }

  if (isEndDate && dttm?.includes('Last')) {
    return now;
  }

  switch (dttm) {
    case 'Last day':
      now.setUTCDate(now.getUTCDate() - 1);
      return now;
    case 'Last week':
      now.setUTCDate(now.getUTCDate() - 7);
      return now;
    case 'Last month':
      now.setUTCMonth(now.getUTCMonth() - 1);
      return now;
    case 'Last quarter':
      now.setUTCMonth(now.getUTCMonth() - 3);
      return now;
    case 'Last year':
      now.setUTCFullYear(now.getUTCFullYear() - 1);
      return now;
    case 'previous calendar week':
      if (isEndDate) {
        now.setDate(now.getDate() - now.getDay() + 1); // end date is the last day of the previous week (Sunday)
      } else {
        now.setDate(now.getDate() - now.getDay() - 6); // start date is the first day of the previous week (Monday)
      }
      return now;
    case 'previous calendar month':
      if (isEndDate) {
        now.setDate(1); // end date is the last day of the previous month
      } else {
        now.setDate(1); // start date is the first day of the previous month
        now.setMonth(now.getMonth() - 1);
      }
      return now;
    case 'previous calendar year':
      if (isEndDate) {
        now.setFullYear(now.getFullYear(), 0, 1); // end date is the last day of the previous year
      } else {
        now.setFullYear(now.getFullYear() - 1, 0, 1); // start date is the first day of the previous year
      }
      return now;
    default:
      break;
  }
  if (dttm?.includes('ago')) {
    const parts = dttm.split(' ');
    const amount = parseInt(parts[0], 10);
    const unit = parts[1];
    switch (unit) {
      case 'day':
      case 'days':
        now.setUTCDate(now.getUTCDate() - amount);
        break;
      case 'week':
      case 'weeks':
        now.setUTCDate(now.getUTCDate() - amount * 7);
        break;
      case 'month':
      case 'months':
        now.setUTCMonth(now.getUTCMonth() - amount);
        break;
      case 'year':
      case 'years':
        now.setUTCFullYear(now.getUTCFullYear() - amount);
        break;
      default:
        break;
    }
    return now;
  }
  const parts = dttm?.split('-');
  let parsed: Date | null = null;
  if (parts && !isEmpty(parts)) {
    if (parts.length === 1) {
      parsed = new Date(Date.UTC(parseInt(parts[0], 10), 0));
    } else if (parts.length === 2) {
      parsed = new Date(
        Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1),
      );
    } else if (parts.length === 3) {
      parsed = new Date(
        parseInt(parts[0], 10),
        parseInt(parts[1], 10) - 1,
        parseInt(parts[2], 10),
      );
    } else {
      parsed = new Date(dttm);
    }
  } else {
    parsed = new Date(dttm);
  }
  if (parsed && !Number.isNaN(parsed.getTime())) {
    if (computingShifts) {
      parsed.setHours(-parsed.getTimezoneOffset() / 60, 0, 0, 0);
    } else {
      parsed.setHours(0, 0, 0, 0);
    }
    return parsed;
  }
  // Return null if the string cannot be parsed into a date
  return null;
};

export const computeCustomDateTime = (
  dttm: string,
  grain: string,
  grainValue: number,
) => {
  let parsed: Date;
  if (dttm === 'now' || dttm === 'today') {
    parsed = new Date();
  } else {
    parsed = new Date(dttm);
  }
  if (!Number.isNaN(parsed.getTime())) {
    switch (grain) {
      case 'second':
        parsed.setSeconds(parsed.getSeconds() + grainValue);
        break;
      case 'minute':
        parsed.setMinutes(parsed.getMinutes() + grainValue);
        break;
      case 'hour':
        parsed.setHours(parsed.getHours() + grainValue);
        break;
      case 'day':
        parsed.setDate(parsed.getDate() + grainValue);
        break;
      case 'week':
        parsed.setDate(parsed.getDate() + grainValue * 7);
        break;
      case 'month':
        parsed.setMonth(parsed.getMonth() + grainValue);
        break;
      case 'quarter':
        parsed.setMonth(parsed.getMonth() + grainValue * 3);
        break;
      case 'year':
        parsed.setFullYear(parsed.getFullYear() + grainValue);
        break;
      default:
        break;
    }
    return parsed;
  }
  return null;
};

type TimeOffsetArgs = {
  timeRangeFilter: any;
  shifts: string[];
  startDate: string;
  includeFutureOffsets?: boolean;
};

export const getTimeOffset = ({
  timeRangeFilter,
  shifts,
  startDate,
  includeFutureOffsets = true,
}: TimeOffsetArgs): string[] => {
  const { customRange, matchedFlag } = customTimeRangeDecode(
    timeRangeFilter?.comparator ?? '',
  );
  let customStartDate: Date | null = null;
  let customEndDate: Date | null = null;
  if (matchedFlag) {
    // Compute the start date and end date using the custom range information
    const {
      sinceDatetime,
      sinceMode,
      sinceGrain,
      sinceGrainValue,
      untilDatetime,
      untilMode,
      untilGrain,
      untilGrainValue,
    } = { ...customRange };
    if (sinceMode !== 'relative') {
      if (sinceMode === 'specific') {
        customStartDate = new Date(sinceDatetime);
      } else {
        customStartDate = parseDttmToDate(sinceDatetime, false, true);
      }
    } else {
      customStartDate = computeCustomDateTime(
        sinceDatetime,
        sinceGrain,
        sinceGrainValue,
      );
    }
    customStartDate?.setHours(0, 0, 0, 0);
    if (untilMode !== 'relative') {
      if (untilMode === 'specific') {
        customEndDate = new Date(untilDatetime);
      } else {
        customEndDate = parseDttmToDate(untilDatetime, false, true);
      }
    } else {
      customEndDate = computeCustomDateTime(
        untilDatetime,
        untilGrain,
        untilGrainValue,
      );
    }
    customEndDate?.setHours(0, 0, 0, 0);
  }
  const isCustom = shifts?.includes('custom');
  const isInherit = shifts?.includes('inherit');
  let customStartDateTime: number | undefined;
  if (isCustom) {
    if (matchedFlag) {
      customStartDateTime = new Date(
        new Date(startDate).setUTCHours(
          new Date(startDate).getTimezoneOffset() / 60,
          0,
          0,
          0,
        ),
      ).getTime();
    } else {
      customStartDateTime = parseDttmToDate(startDate)?.getTime();
    }
  }
  const [startStr, endStr] = (timeRangeFilter?.comparator ?? '')
    .split(' : ')
    .map((date: string) => date.trim());
  const filterStartDateTime =
    (customStartDate ?? parseDttmToDate(startStr, false, false))?.getTime() ||
    0;
  const filterEndDateTime =
    (
      customEndDate ?? parseDttmToDate(endStr || startStr, true, false)
    )?.getTime() || 0;

  const customShift =
    customStartDateTime &&
    Math.round((filterStartDateTime - customStartDateTime) / DAY_IN_MS);
  const inInheritShift =
    isInherit &&
    Math.round((filterEndDateTime - filterStartDateTime) / DAY_IN_MS);

  const newShifts = ensureIsArray(shifts)
    .map(shift => {
      if (shift === 'custom') {
        if (customShift !== undefined && !Number.isNaN(customShift)) {
          if (includeFutureOffsets && customShift < 0) {
            return `${customShift * -1} days after`;
          }
          if (customShift >= 0) {
            return `${customShift} days ago`;
          }
        }
      }
      if (shift === 'inherit') {
        if (inInheritShift && !Number.isNaN(inInheritShift)) {
          if (includeFutureOffsets && inInheritShift < 0) {
            return `${inInheritShift * -1} days after`;
          }
          if (inInheritShift > 0) {
            return `${inInheritShift} days ago`;
          }
        }
      }
      return shift;
    })
    .filter(shift => shift !== 'custom' && shift !== 'inherit');
  return ensureIsArray(newShifts);
};