1import { SynchronousDataTransformerInfo } from '../../types';
2import { map } from 'rxjs/operators';
3
4import { DataTransformerID } from './ids';
5import { DataFrame, Field, FieldType } from '../../types/dataFrame';
6import { dateTimeParse } from '../../datetime';
7import { ArrayVector } from '../../vector';
8import { fieldMatchers } from '../matchers';
9import { FieldMatcherID } from '../matchers/ids';
10
11export interface ConvertFieldTypeTransformerOptions {
12  conversions: ConvertFieldTypeOptions[];
13}
14
15export interface ConvertFieldTypeOptions {
16  /**
17   * The field to convert field type
18   */
19  targetField?: string;
20  /**
21   * The field type to convert to
22   */
23  destinationType?: FieldType;
24  /**
25   * Date format to parse a string datetime
26   */
27  dateFormat?: string;
28}
29
30export const convertFieldTypeTransformer: SynchronousDataTransformerInfo<ConvertFieldTypeTransformerOptions> = {
31  id: DataTransformerID.convertFieldType,
32  name: 'Convert field type',
33  description: 'Convert a field to a specified field type',
34  defaultOptions: {
35    fields: {},
36    conversions: [{ targetField: undefined, destinationType: undefined, dateFormat: undefined }],
37  },
38
39  operator: (options) => (source) => source.pipe(map((data) => convertFieldTypeTransformer.transformer(options)(data))),
40
41  transformer: (options: ConvertFieldTypeTransformerOptions) => (data: DataFrame[]) => {
42    if (!Array.isArray(data) || data.length === 0) {
43      return data;
44    }
45    const timeParsed = convertFieldTypes(options, data);
46    if (!timeParsed) {
47      return [];
48    }
49    return timeParsed;
50  },
51};
52
53/**
54 * Convert field types for dataframe(s)
55 * @param options - field type conversion options
56 * @param frames - dataframe(s) with field types to convert
57 * @returns dataframe(s) with converted field types
58 */
59export function convertFieldTypes(options: ConvertFieldTypeTransformerOptions, frames: DataFrame[]): DataFrame[] {
60  if (!options.conversions.length) {
61    return frames;
62  }
63
64  const framesCopy = frames.map((frame) => ({ ...frame }));
65
66  for (const conversion of options.conversions) {
67    if (!conversion.targetField) {
68      continue;
69    }
70    const matches = fieldMatchers.get(FieldMatcherID.byName).get(conversion.targetField);
71    for (const frame of framesCopy) {
72      frame.fields = frame.fields.map((field) => {
73        if (matches(field, frame, framesCopy)) {
74          return convertFieldType(field, conversion);
75        }
76        return field;
77      });
78    }
79  }
80
81  return framesCopy;
82}
83
84/**
85 * Convert a single field type to specifed field type.
86 * @param field - field to convert
87 * @param opts - field conversion options
88 * @returns converted field
89 *
90 * @internal
91 */
92export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): Field {
93  switch (opts.destinationType) {
94    case FieldType.time:
95      return ensureTimeField(field, opts.dateFormat);
96    case FieldType.number:
97      return fieldToNumberField(field);
98    case FieldType.string:
99      return fieldToStringField(field);
100    case FieldType.boolean:
101      return fieldToBooleanField(field);
102    default:
103      return field;
104  }
105}
106
107// matches ISO 8601, e.g. 2021-11-11T19:45:00.000Z (float portion optional)
108const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
109
110/**
111 * @internal
112 */
113export function fieldToTimeField(field: Field, dateFormat?: string): Field {
114  let opts = dateFormat ? { format: dateFormat } : undefined;
115
116  const timeValues = field.values.toArray().slice();
117
118  let firstDefined = timeValues.find((v) => v != null);
119
120  let isISO8601 = typeof firstDefined === 'string' && iso8601Regex.test(firstDefined);
121
122  for (let t = 0; t < timeValues.length; t++) {
123    if (timeValues[t]) {
124      let parsed = isISO8601 ? Date.parse(timeValues[t]) : dateTimeParse(timeValues[t], opts).valueOf();
125      timeValues[t] = Number.isFinite(parsed) ? parsed : null;
126    } else {
127      timeValues[t] = null;
128    }
129  }
130
131  return {
132    ...field,
133    type: FieldType.time,
134    values: new ArrayVector(timeValues),
135  };
136}
137
138function fieldToNumberField(field: Field): Field {
139  const numValues = field.values.toArray().slice();
140
141  for (let n = 0; n < numValues.length; n++) {
142    const number = +numValues[n];
143    numValues[n] = Number.isFinite(number) ? number : null;
144  }
145
146  return {
147    ...field,
148    type: FieldType.number,
149    values: new ArrayVector(numValues),
150  };
151}
152
153function fieldToBooleanField(field: Field): Field {
154  const booleanValues = field.values.toArray().slice();
155
156  for (let b = 0; b < booleanValues.length; b++) {
157    booleanValues[b] = Boolean(!!booleanValues[b]);
158  }
159
160  return {
161    ...field,
162    type: FieldType.boolean,
163    values: new ArrayVector(booleanValues),
164  };
165}
166
167function fieldToStringField(field: Field): Field {
168  const stringValues = field.values.toArray().slice();
169
170  for (let s = 0; s < stringValues.length; s++) {
171    stringValues[s] = `${stringValues[s]}`;
172  }
173
174  return {
175    ...field,
176    type: FieldType.string,
177    values: new ArrayVector(stringValues),
178  };
179}
180
181/**
182 * Checks the first value. Assumes any number should be time fieldtype. Otherwise attempts to make the fieldtype time.
183 * @param field - field to ensure is a time fieldtype
184 * @param dateFormat - date format used to parse a string datetime
185 * @returns field as time
186 *
187 * @public
188 */
189export function ensureTimeField(field: Field, dateFormat?: string): Field {
190  const firstValueTypeIsNumber = typeof field.values.get(0) === 'number';
191  if (field.type === FieldType.time && firstValueTypeIsNumber) {
192    return field; //already time
193  }
194  if (firstValueTypeIsNumber) {
195    return {
196      ...field,
197      type: FieldType.time, //assumes it should be time
198    };
199  }
200  return fieldToTimeField(field, dateFormat);
201}
202