1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {Timestamp} from 'common/time';
18import {TIME_UNITS} from './time_units';
19
20export class TimestampUtils {
21  // (?=.) checks there is at least one character with a lookahead match
22  private static readonly REAL_TIME_ONLY_REGEX =
23    /^(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
24  private static readonly REAL_DATE_TIME_REGEX =
25    /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9])),\s(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
26  private static readonly ISO_TIMESTAMP_REGEX =
27    /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
28  private static readonly ELAPSED_TIME_REGEX =
29    /^(?=.)([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+ns)?$/;
30  private static readonly NS_TIME_REGEX = /^\s*[0-9]+(\s?ns)?\s*$/;
31
32  static isNsFormat(timestampHuman: string): boolean {
33    return TimestampUtils.NS_TIME_REGEX.test(timestampHuman);
34  }
35
36  static isHumanElapsedTimeFormat(timestampHuman: string): boolean {
37    return TimestampUtils.ELAPSED_TIME_REGEX.test(timestampHuman);
38  }
39
40  static isRealTimeOnlyFormat(timestampHuman: string): boolean {
41    return TimestampUtils.REAL_TIME_ONLY_REGEX.test(timestampHuman);
42  }
43
44  static isRealDateTimeFormat(timestampHuman: string): boolean {
45    return TimestampUtils.REAL_DATE_TIME_REGEX.test(timestampHuman);
46  }
47
48  static isISOFormat(timestampHuman: string): boolean {
49    return TimestampUtils.ISO_TIMESTAMP_REGEX.test(timestampHuman);
50  }
51
52  static isHumanRealTimestampFormat(timestampHuman: string): boolean {
53    return (
54      TimestampUtils.isISOFormat(timestampHuman) ||
55      TimestampUtils.isRealDateTimeFormat(timestampHuman) ||
56      TimestampUtils.isRealTimeOnlyFormat(timestampHuman)
57    );
58  }
59
60  static extractDateFromHumanTimestamp(
61    timestampHuman: string,
62  ): string | undefined {
63    if (
64      !TimestampUtils.isRealDateTimeFormat(timestampHuman) &&
65      !TimestampUtils.isISOFormat(timestampHuman)
66    ) {
67      return undefined;
68    }
69    return timestampHuman.slice(0, 10);
70  }
71
72  static extractTimeFromHumanTimestamp(
73    timestampHuman: string,
74  ): string | undefined {
75    if (TimestampUtils.isRealDateTimeFormat(timestampHuman)) {
76      return timestampHuman.slice(12);
77    }
78    if (TimestampUtils.isISOFormat(timestampHuman)) {
79      return timestampHuman.slice(11);
80    }
81    if (TimestampUtils.isRealTimeOnlyFormat(timestampHuman)) {
82      return timestampHuman;
83    }
84    return undefined;
85  }
86
87  static compareFn(a: Timestamp, b: Timestamp): number {
88    return Number(a.getValueNs() - b.getValueNs());
89  }
90
91  static min(ts1: Timestamp, ts2: Timestamp): Timestamp {
92    if (ts2.getValueNs() < ts1.getValueNs()) {
93      return ts2;
94    }
95
96    return ts1;
97  }
98
99  static max(ts1: Timestamp, ts2: Timestamp): Timestamp {
100    if (ts2.getValueNs() > ts1.getValueNs()) {
101      return ts2;
102    }
103
104    return ts1;
105  }
106
107  static formatElapsedNs(timestampNanos: bigint): string {
108    let leftNanos = timestampNanos;
109    const parts: Array<{value: bigint; unit: string}> = TIME_UNITS.slice()
110      .reverse()
111      .map(({nanosInUnit, unit}) => {
112        let amountOfUnit = BigInt(0);
113        if (leftNanos >= nanosInUnit) {
114          amountOfUnit = leftNanos / BigInt(nanosInUnit);
115        }
116        leftNanos = leftNanos % BigInt(nanosInUnit);
117        return {value: amountOfUnit, unit};
118      });
119
120    // Remove all 0ed units at start
121    while (parts.length > 1 && parts[0].value === 0n) {
122      parts.shift();
123    }
124
125    return parts.map((part) => `${part.value}${part.unit}`).join('');
126  }
127}
128