@marketto/belfiore-connector-embedded
Version:
TS & JS Belfiore datasource connector: embedded dataset
495 lines (462 loc) • 13.4 kB
text/typescript
import dayjs, { Dayjs } from "dayjs";
import {
BelfioreAbstractConnector,
BelfiorePlace,
IBelfioreCity,
IBelfioreCommonPlace,
IBelfioreCountry,
MultiFormatDate,
} from "@marketto/belfiore-connector";
import generatorWrapper from "../functions/generator-wrapper.function";
import type IGeneratorWrapper from "../interfaces/generator-wrapper.interface";
import type IBelfioreDbData from "../interfaces/belfiore-db-data.interface";
import type IBelfioreDbLicense from "../interfaces/belfiore-db-license.interface";
import type BelfioreConnectorConfig from "../types/belfiore-connector-config.type";
/**
* Handler for cities and countries Dataset
*/
export default class BelfioreConnector extends BelfioreAbstractConnector {
/**
* Binary find Index (works ONLY in sorted arrays)
* @param text Unique string of values of the same length (step)
* @param value Exact text to find
* @param start text start index for seeking the value
* @param end text end index for seeking the value
* @param step length of a single value to seek properly the text string
* @returns Found value Index or -1 if not found
* @private
*/
private binaryfindIndex(
sourceString: string,
targetText: string,
start: number = 0,
end: number = sourceString.length - 1
): number {
if (!sourceString.length) {
return -1;
}
const rangedStart: number = Math.max(start, 0);
const rangedEnd: number = Math.min(end, sourceString.length - 1);
const currentLength: number = rangedEnd - rangedStart + 1;
if (rangedStart > rangedEnd || currentLength % targetText.length) {
return -1;
}
const targetIndex: number =
rangedStart +
Math.floor(currentLength / (2 * targetText.length)) * targetText.length;
const targetValue: string = sourceString.substr(
targetIndex,
targetText.length
);
if (targetValue === targetText) {
return Math.ceil((targetIndex + 1) / targetText.length) - 1;
}
if (targetText > targetValue) {
return this.binaryfindIndex(
sourceString,
targetText,
targetIndex + targetText.length,
rangedEnd
);
}
return this.binaryfindIndex(
sourceString,
targetText,
rangedStart,
targetIndex - 1
);
}
/**
* Converts belfiore code into an int
*/
private belfioreToInt(code: string): number {
const upperCaseCode: string = code.toUpperCase();
return (
(upperCaseCode.charCodeAt(0) - 65) * 10 ** 3 +
parseInt(upperCaseCode.substr(1), 10)
);
}
/**
* Converts int to belfiore code
* @param code Belfiore int code
* @returns Standard belfiore code
*/
private belfioreFromInt(code: number): string {
const charIndex: number = Math.floor(code / 10 ** 3);
const char: string = String.fromCharCode(charIndex + 65);
const numValue: string = code.toString().substr(-3);
return `${char}${numValue.padStart(3, "0")}`;
}
/**
* Converst Base 32 number of days since 01/01/1861 to Date instance
* @param base32daysFrom1861 Base 32 number of days from 1861-01-01
* @returns Date instance
*/
private decodeDate(base32daysFrom1861: string): Dayjs {
const italyBirthDatePastDays = parseInt(base32daysFrom1861, 32);
return dayjs(this.ITALY_KINGDOM_BIRTHDATE).add(
italyBirthDatePastDays,
"days"
);
}
/**
* Retrieve string at index posizion
* @param list concatenation of names
* @param index target name index
* @returns index-th string
*/
private static nameByIndex(list: string, index: number): string {
if (typeof list !== "string") {
throw new Error(
"[BelfioreConnector.nameByIndex] Provided list is not a string"
);
}
if (!list.length) {
throw new Error("[BelfioreConnector.nameByIndex] Provided list empty");
}
let startIndex: number = 0;
let endIndex: number = list.indexOf("|", startIndex + 1);
let counter: number = index;
while (counter > 0 && endIndex > startIndex) {
counter--;
startIndex = endIndex + 1;
endIndex = list.indexOf("|", startIndex + 1);
}
if (index < 0 || counter > 0) {
throw new Error(
`[BelfioreConnector.nameByIndex] Provided index ${index} is out range`
);
}
if (!counter && endIndex < 0) {
return list.substring(startIndex);
}
return list.substring(startIndex, endIndex);
}
private data: IBelfioreDbData[];
private licenses: IBelfioreDbLicense[];
private sources: string[];
private toDate: Date | undefined;
private fromDate: Date | undefined;
private codeMatcher: RegExp | undefined;
private province: string | undefined;
constructor({
fromDate,
toDate,
codeMatcher,
data,
licenses,
province,
sources,
}: BelfioreConnectorConfig) {
super();
if (codeMatcher && province) {
throw new Error(
"Both codeMatcher and province were provided to Bolfiore, only one is allowed"
);
}
if (toDate && !fromDate) {
throw new Error("Parameter fromDate is mandatory passing toDate");
}
this.fromDate = fromDate;
this.toDate = toDate;
this.codeMatcher = codeMatcher;
this.data = data;
this.licenses = licenses;
this.province = province;
this.sources = sources;
}
private get config(): BelfioreConnectorConfig {
const { codeMatcher, data, fromDate, licenses, sources, toDate } = this;
return {
codeMatcher,
data,
fromDate,
licenses,
sources,
toDate,
} as BelfioreConnectorConfig;
}
private *scanDataSourceIndex(
dataSource: IBelfioreDbData,
matcher?: RegExp
): Generator {
if (matcher) {
for (
let startIndex = 0, entryIndex = 0;
startIndex < dataSource.name.length;
entryIndex++
) {
const endIndex =
dataSource.name.indexOf("|", startIndex + 1) + 1 ||
dataSource.name.length + 1;
const targetName = dataSource.name.substring(startIndex, endIndex - 1);
if (matcher.test(targetName)) {
yield entryIndex;
}
// Moving to next entry to check
startIndex = endIndex;
}
} else {
const dsLength = dataSource.belfioreCode.length / 3;
for (let index = 0; index < dsLength; index++) {
yield index;
}
}
return -1;
}
private scanData(
name?: string | RegExp
): IGeneratorWrapper<BelfiorePlace, null, void> {
return generatorWrapper(this.scanDataGenerator(name));
}
private *scanDataGenerator(name?: string | RegExp): Generator {
const nameMatcher = typeof name === "string" ? new RegExp(name, "i") : name;
for (const sourceData of this.data) {
const dataSourceScan = this.scanDataSourceIndex(sourceData, nameMatcher);
for (
let dss = dataSourceScan.next();
!dss.done;
dss = dataSourceScan.next()
) {
const index = dss.value as number;
const parsedPlace: BelfiorePlace | null = this.locationByIndex(
sourceData,
index
);
if (parsedPlace) {
yield parsedPlace;
}
}
}
return null;
}
/**
* Retrieve location for the given index in the given subset
* @param resourceData concatenation of names
* @param index target name index
* @returns location
*/
private locationByIndex(
resourceData: IBelfioreDbData,
index: number
): BelfiorePlace | null {
const belfioreIndex = index * 3;
if (resourceData.belfioreCode.length - belfioreIndex < 3) {
return null;
}
const belFioreInt = parseInt(
resourceData.belfioreCode.substring(belfioreIndex, belfioreIndex + 3),
32
);
const belfioreCode = this.belfioreFromInt(belFioreInt);
const code = resourceData.provinceOrCountry.substring(
index * 2,
index * 2 + 2
);
if (
(this.province && this.province !== code) ||
(this.codeMatcher && !this.codeMatcher.test(belfioreCode))
) {
return null;
}
const dateIndex = index * 4;
const creationDate = this.decodeDate(
(resourceData.creationDate || "").substring(dateIndex, dateIndex + 4) ||
"0"
).startOf("day");
const expirationDate = this.decodeDate(
(resourceData.expirationDate || "").substring(dateIndex, dateIndex + 4) ||
"2qn13"
).endOf("day");
if (
(this.fromDate &&
resourceData.expirationDate &&
dayjs(this.fromDate).isAfter(expirationDate, "day")) ||
(this.toDate &&
resourceData.creationDate &&
dayjs(this.toDate).isBefore(creationDate, "day"))
) {
return null;
}
const name = BelfioreConnector.nameByIndex(resourceData.name, index);
const licenseIndex = parseInt(resourceData.dataSource, 32)
.toString(2)
.padStart((resourceData.belfioreCode.length * 2) / 3, "0")
.substring(index * 2, index * 2 + 2);
const dataSource = this.licenses[parseInt(licenseIndex, 2)];
const location: IBelfioreCommonPlace = {
belfioreCode,
creationDate: creationDate.toDate(),
dataSource,
expirationDate: expirationDate.toDate(),
name,
};
const isCountry = belfioreCode[0] === "Z";
if (isCountry) {
return {
...location,
iso3166: code,
} as IBelfioreCountry;
}
return {
...location,
province: code,
} as IBelfioreCity;
}
private parseProvinces(): string[] {
const provinceList = new Set<string>();
for (const sourceData of this.data) {
const dataSourceScan = this.scanDataSourceIndex(sourceData);
for (
let dss = dataSourceScan.next();
!dss.done;
dss = dataSourceScan.next()
) {
const index = dss.value as number;
const province = sourceData.provinceOrCountry.substr(index * 2, 2);
if (!provinceList.has(province)) {
const belFioreInt = parseInt(
sourceData.belfioreCode.substr(index * 3, 3),
32
);
const belfioreCode = this.belfioreFromInt(belFioreInt);
if (this.CITY_CODE_MATCHER.test(belfioreCode)) {
if (province.trim()) {
provinceList.add(province);
}
}
}
}
}
return Array.from(provinceList);
}
/**
* Return belfiore places list
*/
public async toArray(): Promise<BelfiorePlace[]> {
return [...this.scanData()] as BelfiorePlace[];
}
public get provinces(): Promise<string[]> {
return new Promise((resolve) => {
if (this.province) {
resolve([this.province]);
} else if (this.codeMatcher !== this.COUNTRY_CODE_MATCHER) {
resolve(this.parseProvinces());
} else {
resolve([]);
}
});
}
/**
* @description Search places matching given name
*/
public async searchByName(name: string): Promise<BelfiorePlace[] | null> {
return name ? ([...this.scanData(name)] as BelfiorePlace[]) : null;
}
/**
* @description Find place matching given name, retuns place object if provided name match only 1 result
*/
public async findByName(name: string): Promise<BelfiorePlace | null> {
if (!name) {
return null;
}
const startingNameMatcher = new RegExp(`^${name}$`, "i");
return this.scanData(startingNameMatcher).next().value;
}
/**
* @description Retrieve Place by Belfiore Code
*/
public async findByCode(belfioreCode: string): Promise<BelfiorePlace | null> {
if (this.BELFIORE_CODE_MATCHER.test(belfioreCode)) {
const base32name: string = this.belfioreToInt(belfioreCode)
.toString(32)
.padStart(3, "0");
for (const sourceData of this.data || []) {
const index: number = this.binaryfindIndex(
sourceData.belfioreCode,
base32name
);
if (index >= 0) {
return this.locationByIndex(sourceData, index);
}
}
}
return null;
}
/**
* Returns a Proxied version of Belfiore which filters results by given date
* @param date Target date to filter places active only for the given date
* @returns Belfiore instance filtered by active date
* @public
*/
public active(date: MultiFormatDate = new Date()): BelfioreConnector {
return new BelfioreConnector({
...this.config,
fromDate: Array.isArray(date)
? new Date(date[0], date[1] ?? 0, date[2] ?? 1)
: dayjs(date).toDate(),
toDate: Array.isArray(date)
? new Date(date[0], date[1] ?? 0, date[2] ?? 1)
: dayjs(date).toDate(),
});
}
/**
* Returns a Proxied version of Belfiore which filters results by given date ahead
* @param date Target date to filter places active only for the given date
* @returns Belfiore instance filtered by active date
* @public
*/
public from(date: MultiFormatDate = new Date()): BelfioreConnector {
return new BelfioreConnector({
...this.config,
fromDate: Array.isArray(date)
? new Date(date[0], date[1] ?? 0, date[2] ?? 1)
: dayjs(date).toDate(),
});
}
/**
* Returns a Belfiore instance filtered by the given province
* @param code Province Code (2 A-Z char)
* @returns Belfiore instance filtered by province code
* @public
*/
public byProvince(code: string): BelfioreConnector | undefined {
if (typeof code !== "string" || !/^[A-Z]{2}$/u.test(code)) {
return;
}
return new BelfioreConnector({
...this.config,
codeMatcher: undefined,
province: code,
});
}
/**
* Returns a Proxied version of Belfiore which filters results by place type
*/
public get cities(): BelfioreConnector | undefined {
if (this.codeMatcher && this.codeMatcher !== this.CITY_CODE_MATCHER) {
return undefined;
}
return new BelfioreConnector({
...this.config,
codeMatcher: this.CITY_CODE_MATCHER,
province: undefined,
});
}
/**
* Returns a Proxied version of Belfiore which filters results by place type
*/
public get countries(): BelfioreConnector | undefined {
if (
(this.codeMatcher && this.codeMatcher !== this.COUNTRY_CODE_MATCHER) ||
this.province
) {
return undefined;
}
return new BelfioreConnector({
...this.config,
codeMatcher: this.COUNTRY_CODE_MATCHER,
province: undefined,
});
}
}