Skip to content
Open
10 changes: 10 additions & 0 deletions src/parsers/field-parser-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type FieldParserRawValue = string | number | boolean;

export interface FieldParserInterface<T> {
/**
* Parse the raw value and return a value of type T or undefined if unparseable
*
* @param rawValue T | undefined
*/
parseValue(rawValue: FieldParserRawValue): T | undefined;
}
35 changes: 35 additions & 0 deletions src/parsers/field-types/boolean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from 'vitest';

import { BooleanParser } from './boolean';

describe('BooleanParser', () => {
test('can parse string number truthy', () => {
const parser = new BooleanParser();
const response = parser.parseValue('1');
expect(response).toBe(true);
});

test('can parse string number falsy', () => {
const parser = new BooleanParser();
const response = parser.parseValue('0');
expect(response).toBe(false);
});

test('can parse words truthy', () => {
const parser = new BooleanParser();
const response = parser.parseValue('true');
expect(response).toBe(true);
});

test('can parse words falsy', () => {
const parser = new BooleanParser();
const response = parser.parseValue('false');
expect(response).toBe(false);
});

test('can parse date truthy', () => {
const parser = new BooleanParser();
const response = parser.parseValue(Date());
expect(response).toBe(true);
});
});
21 changes: 21 additions & 0 deletions src/parsers/field-types/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
FieldParserInterface,
FieldParserRawValue,
} from '../field-parser-interface';

export class BooleanParser implements FieldParserInterface<boolean> {
// use a shared static instance for performance instead of
// instantiating a new instance for every use
static shared = new BooleanParser();

/** @inheritdoc */
parseValue(rawValue: FieldParserRawValue): boolean {
if (
typeof rawValue === 'string' &&
(rawValue === 'false' || rawValue === '0')
) {
return false;
}
return Boolean(rawValue);
}
}
35 changes: 35 additions & 0 deletions src/parsers/field-types/byte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from 'vitest';

import { ByteParser } from './byte';

describe('ByteParser', () => {
test('can parse int strings', () => {
const parser = new ByteParser();
const response = parser.parseValue('3');
expect(response).toBe(3);
});

test('can parse float strings', () => {
const parser = new ByteParser();
const response = parser.parseValue('3.14');
expect(response).toBe(3.14);
});

test('returns undefined if the number is not a number', () => {
const parser = new ByteParser();
const response = parser.parseValue('qab');
expect(response).toBeUndefined();
});

test('returns the number if a number is passed', () => {
const parser = new ByteParser();
const response = parser.parseValue(5.67);
expect(response).toBe(5.67);
});

test('returns undefined if a boolean is passed in', () => {
const parser = new ByteParser();
const response = parser.parseValue(true);
expect(response).toBeUndefined();
});
});
30 changes: 30 additions & 0 deletions src/parsers/field-types/byte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
FieldParserInterface,
FieldParserRawValue,
} from '../field-parser-interface';
import { NumberParser } from './number';

/**
* A Byte is a unit-specific `number`, in bytes.
*/
export type Byte = number;

/**
* The ByteParser is a unit-specific NumberParser
* that returns a value in bytes
*
* @export
* @class ByteParser
* @implements {FieldParserInterface<Byte>}
*/
export class ByteParser implements FieldParserInterface<Byte> {
// use a shared static instance for performance instead of
// instantiating a new instance for every use
static shared = new ByteParser();

/** @inheritdoc */
parseValue(rawValue: FieldParserRawValue): Byte | undefined {
const parser = NumberParser.shared;
return parser.parseValue(rawValue);
}
}
137 changes: 137 additions & 0 deletions src/parsers/field-types/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, test } from 'vitest';

import { DateParser } from './date';

describe('DateParser', () => {
test('can parse date-only strings', () => {
const parser = new DateParser();
const response = parser.parseValue('2020-06-20');
const response2 = parser.parseValue('06/20/2020');
const expected = new Date();
expected.setHours(0);
expected.setMinutes(0);
expected.setSeconds(0);
expected.setMilliseconds(0);
expected.setMonth(5);
expected.setDate(20);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
expect(response2?.getTime()).toBe(expected.getTime());
});

test('can parse date-time strings', () => {
const parser = new DateParser();
const response = parser.parseValue('2020-06-20 3:46:23');
const response2 = parser.parseValue('2020-06-20 03:46:23');
const expected = new Date();
expected.setHours(3);
expected.setMinutes(46);
expected.setSeconds(23);
expected.setMilliseconds(0);
expected.setMonth(5);
expected.setDate(20);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
expect(response2?.getTime()).toBe(expected.getTime());
});

test('can parse date-time strings different string test', () => {
const parser = new DateParser();
const response = parser.parseValue('2020-09-20 05:12:38');
const expected = new Date();
expected.setHours(5);
expected.setMinutes(12);
expected.setSeconds(38);
expected.setMilliseconds(0);
expected.setMonth(8);
expected.setDate(20);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
});

test('can parse ISO8601 strings without time zones', () => {
const parser = new DateParser();
const response = parser.parseValue('2020-06-20T13:37:15');
const expected = new Date();
expected.setHours(13);
expected.setMinutes(37);
expected.setSeconds(15);
expected.setMilliseconds(0);
expected.setMonth(5);
expected.setDate(20);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
});

test('can parse ISO8601 strings with explicit time zones', () => {
const parser = new DateParser();
const response = parser.parseValue('2020-06-20T13:37:15Z');
const response2 = parser.parseValue('2020-06-20T13:37:15-00:00');
const response3 = parser.parseValue('2020-06-20T13:37:15+00:00');
const expected = new Date();
expected.setUTCHours(13);
expected.setUTCMinutes(37);
expected.setUTCSeconds(15);
expected.setUTCMilliseconds(0);
expected.setUTCMonth(5);
expected.setUTCDate(20);
expected.setUTCFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
expect(response2?.getTime()).toBe(expected.getTime());
expect(response3?.getTime()).toBe(expected.getTime());
});

test('can parse "c.a. yyyy" formatted dates', () => {
const parser = new DateParser();
const response = parser.parseValue('c.a. 2020');
const expected = new Date();
expected.setHours(0);
expected.setMinutes(0);
expected.setSeconds(0);
expected.setMilliseconds(0);
expected.setMonth(0);
expected.setDate(1);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
});

test('can parse "ca yyyy" formatted dates', () => {
const parser = new DateParser();
const response = parser.parseValue('ca 2020');
const expected = new Date();
expected.setHours(0);
expected.setMinutes(0);
expected.setSeconds(0);
expected.setMilliseconds(0);
expected.setMonth(0);
expected.setDate(1);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
});

test('can parse "[yyyy]" formatted dates', () => {
const parser = new DateParser();
const response = parser.parseValue('[2020]');
const expected = new Date();
expected.setHours(0);
expected.setMinutes(0);
expected.setSeconds(0);
expected.setMilliseconds(0);
expected.setMonth(0);
expected.setDate(1);
expected.setFullYear(2020);
expect(response?.getTime()).toBe(expected.getTime());
});

test('returns undefined if it is a bad date', () => {
const parser = new DateParser();
const response = parser.parseValue('absjkdvfnskj');
expect(response).toBeUndefined();
});

test('returns undefined if passed a non-string value', () => {
const parser = new DateParser();
const response = parser.parseValue(123);
expect(response).toBeUndefined();
});
});
57 changes: 57 additions & 0 deletions src/parsers/field-types/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
FieldParserInterface,
FieldParserRawValue,
} from '../field-parser-interface';

export class DateParser implements FieldParserInterface<Date> {
// use a shared static instance for performance instead of
// instantiating a new instance for every use
static shared = new DateParser();

/** @inheritdoc */
parseValue(rawValue: FieldParserRawValue): Date | undefined {
// try different date parsing
return this.parseJSDate(rawValue) || this.parseBracketDate(rawValue);
}

// handles "[yyyy]" format
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QUESTION: Are there any month/year formats we need to support? (BTW: @ximm is a good reference for dates we support throughout the system.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially? This PR is just for migrating from a standalone repo into elements, not modifying any functionality. I filed https://webarchive.jira.com/browse/WEBDEV-8535 to investigate that.

Copy link
Copy Markdown

@ximm ximm Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QUESTION: Are there any month/year formats we need to support? (BTW: @ximm is a good reference for dates we support throughout the system.)

I can talk about dates, but would need context.

(In brief, forgiving raw string formats with enormous variability and many styles are explicitly allowed in the date field in item metadata; these are normalized in search documents into ISO8601 timestamps, and year derived when possible. Related, I just merged a chance today that will extend the representation of dates in all search documents, to exprsss a range, with a best-effort precision and uncertainty assignment.)

private parseBracketDate(rawValue: FieldParserRawValue): Date | undefined {
if (typeof rawValue !== 'string') return undefined;
const yearMatch = rawValue.match(/\[([0-9]{4})\]/);
if (!yearMatch || yearMatch.length < 2) {
return undefined;
}
return this.parseJSDate(yearMatch[1]);
}

private parseJSDate(rawValue: FieldParserRawValue): Date | undefined {
if (typeof rawValue !== 'string') return undefined;
let parsedValue = rawValue;

// fix for Safari not supporting `yyyy-mm-dd HH:MM:SS` format, insert a `T` into the space
if (
parsedValue.match(
/^[0-9]{4}-[0-9]{2}-[0-9]{2}\s{1}[0-9]{2}:[0-9]{2}:[0-9]{2}$/,
)
) {
parsedValue = parsedValue.replace(' ', 'T');
}

const parsed = Date.parse(parsedValue);
if (Number.isNaN(parsed)) {
return undefined;
}
let date = new Date(parsedValue);
// The `Date(string)` constructor parses some strings as UTC and some in the local timezone.
// This attempts to detect cases that get parsed as UTC but should be parsed as local.
// Note that this does _not_ include cases with an explicit time zone specified, which
// should generally be parsed as-is and not converted to local time.
const isUTCTimeZoneInferred =
parsedValue.match(/^[0-9]{4}$/) || // just the year, ie `2020`
parsedValue.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/); // YYYY-MM-DD format
if (isUTCTimeZoneInferred) {
date = new Date(date.getTime() + date.getTimezoneOffset() * 1000 * 60);
}
return date;
}
}
49 changes: 49 additions & 0 deletions src/parsers/field-types/duration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from 'vitest';

import { DurationParser } from './duration';

describe('DurationParser', () => {
test('can parse mm:ss format', () => {
const parser = new DurationParser();
const response = parser.parseValue('45:23');
const expected = 23 + 45 * 60;
expect(response).toBe(expected);
});

test('can parse hh:mm:ss format', () => {
const parser = new DurationParser();
const response = parser.parseValue('3:45:23');
const expected = 23 + 45 * 60 + 3 * 60 * 60;
expect(response).toBe(expected);
});

test('returns undefined for a non-number component in hh:mm:ss format', () => {
const parser = new DurationParser();
const response = parser.parseValue('3:AB:23');
expect(response).toBeUndefined();
});

test('can parse decimal format', () => {
const parser = new DurationParser();
const response = parser.parseValue('345.23');
expect(response).toBe(345.23);
});

test('returns undefined for non-numeric numbers', () => {
const parser = new DurationParser();
const response = parser.parseValue('abc.de');
expect(response).toBeUndefined();
});

test('returns the number if passed a number', () => {
const parser = new DurationParser();
const response = parser.parseValue(345.23);
expect(response).toBe(345.23);
});

test('returns undefined if passed a boolean', () => {
const parser = new DurationParser();
const response = parser.parseValue(true);
expect(response).toBeUndefined();
});
});
Loading
Loading