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