Skip to content

Commit b6eb2e7

Browse files
committed
Revision #3
1 parent c794c4b commit b6eb2e7

File tree

12 files changed

+878
-523
lines changed

12 files changed

+878
-523
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@types/babel__preset-env": "7.9.7",
6161
"@types/eslint": "8.56.12",
6262
"@types/jest": "^29.5.14",
63-
"@types/node": "^24.3.1",
63+
"@types/node": "22.14.1",
6464
"husky": "7.0.4",
6565
"jest": "^29.7.0",
6666
"lint-staged": "11.2.6",

packages/sources/ftse-sftp/src/parsing/base-parser.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { parse, Options } from 'csv-parse/sync'
66
* Uses the csv-parse library for robust CSV parsing
77
*/
88
export abstract class BaseCSVParser implements CSVParser {
9-
protected config: Record<string, any>
9+
protected config: Options
1010

11-
constructor(config: Record<string, any> = {}) {
11+
constructor(config: Options = {}) {
1212
this.config = { ...config }
1313
}
1414

@@ -17,12 +17,6 @@ export abstract class BaseCSVParser implements CSVParser {
1717
*/
1818
abstract parse(csvContent: string): Promise<ParsedData[]>
1919

20-
/**
21-
* Abstract method that must be implemented by concrete classes
22-
* Should validate the CSV content format and structure
23-
*/
24-
abstract validateFormat(csvContent: string): boolean
25-
2620
/**
2721
* Helper method to parse CSV content using csv-parse library
2822
*/
@@ -49,18 +43,18 @@ export abstract class BaseCSVParser implements CSVParser {
4943

5044
switch (expectedType) {
5145
case 'number': {
52-
const numValue = parseFloat(trimmedValue.replace(/,/g, ''))
46+
const numValue = parseFloat(value.replace(/,/g, ''))
5347
return isNaN(numValue) ? null : numValue
5448
}
5549

5650
case 'date': {
57-
const dateValue = new Date(trimmedValue)
51+
const dateValue = new Date(value)
5852
return isNaN(dateValue.getTime()) ? null : dateValue
5953
}
6054

6155
case 'string':
6256
default:
63-
return trimmedValue
57+
return value
6458
}
6559
}
6660
}

packages/sources/ftse-sftp/src/parsing/factory.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ export class CSVParserFactory {
2323
case 'FTSE100INDEX':
2424
return new FTSE100Parser()
2525
case 'Russell1000INDEX':
26-
return new RussellDailyValuesParser(instrumentToElementMap[instrument])
2726
case 'Russell2000INDEX':
28-
return new RussellDailyValuesParser(instrumentToElementMap[instrument])
2927
case 'Russell3000INDEX':
3028
return new RussellDailyValuesParser(instrumentToElementMap[instrument])
3129
default:

packages/sources/ftse-sftp/src/parsing/ftse100.ts

Lines changed: 9 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const FTSE_INDEX_SECTOR_NAME_COLUMN = 'Index/Sector Name'
1010
const FTSE_NUMBER_OF_CONSTITUENTS_COLUMN = 'Number of Constituents'
1111
const FTSE_INDEX_BASE_CURRENCY_COLUMN = 'Index Base Currency'
1212
const FTSE_GBP_INDEX_COLUMN = 'GBP Index'
13+
const FIRST_DATA_ROW = 4 // Start from header line (line 4)
1314

1415
/**
1516
* Specific data structure for FTSE data
@@ -41,31 +42,20 @@ export class FTSE100Parser extends BaseCSVParser {
4142
}
4243

4344
async parse(csvContent: string): Promise<FTSE100Data[]> {
44-
if (!this.validateFormat(csvContent)) {
45-
throw new Error('Invalid CSV format for FTSE data')
46-
}
47-
4845
const parsed = this.parseCSV(csvContent, {
49-
from_line: 4, // Start parsing from line 4 (includes header)
46+
from_line: FIRST_DATA_ROW, // Start parsing from line 5 (includes header)
5047
})
51-
const results: FTSE100Data[] = []
52-
53-
for (const row of parsed) {
54-
try {
55-
// Only include records where indexCode is "UKX" (FTSE 100 Index)
56-
if (row[FTSE_INDEX_CODE_COLUMN] != FTSE_100_INDEX_CODE) {
57-
continue
58-
}
5948

60-
const data = this.createFTSE100Data(row)
61-
results.push(data)
62-
} catch (error) {
63-
console.error(`Error parsing row:`, error)
64-
}
65-
}
49+
const results: FTSE100Data[] = parsed
50+
.filter((row) => {
51+
return row[FTSE_INDEX_CODE_COLUMN] === FTSE_100_INDEX_CODE
52+
})
53+
.map((row) => this.createFTSE100Data(row))
6654

6755
if (results.length > 1) {
6856
throw new Error('Multiple FTSE 100 index records found, expected only one')
57+
} else if (results.length === 0) {
58+
throw new Error('No FTSE 100 index record found')
6959
}
7060

7161
return results
@@ -102,53 +92,4 @@ export class FTSE100Parser extends BaseCSVParser {
10292
gbpIndex: this.convertValue(row[FTSE_GBP_INDEX_COLUMN], 'number') as number | null,
10393
}
10494
}
105-
106-
/**
107-
* Validate that the first 2 rows actually match the expected FTSE format
108-
*/
109-
validateFormat(csvContent: string): boolean {
110-
if (!csvContent || csvContent.trim().length === 0) {
111-
return false
112-
}
113-
114-
try {
115-
// Parse from line 4 (header) to line 6 to validate the format
116-
const parsed = this.parseCSV(csvContent, {
117-
from_line: 4,
118-
to_line: 6,
119-
relax_column_count: true,
120-
})
121-
122-
if (!parsed || parsed.length === 0) {
123-
return false
124-
}
125-
126-
// Check if we can access the expected columns from the first data row
127-
const firstDataRow = parsed[0]
128-
if (!firstDataRow) {
129-
console.error('No data rows found in CSV for validation')
130-
return false
131-
}
132-
133-
const requiredColumns = [
134-
'Index Code',
135-
'Index/Sector Name',
136-
'Number of Constituents',
137-
'Index Base Currency',
138-
'GBP Index',
139-
]
140-
141-
const missingColumns = requiredColumns.filter((column) => firstDataRow[column] === undefined)
142-
143-
if (missingColumns.length > 0) {
144-
console.error(`Missing required columns in FTSE CSV: ${missingColumns.join(', ')}`)
145-
console.error(`Available columns: ${Object.keys(firstDataRow).join(', ')}`)
146-
return false
147-
}
148-
149-
return true
150-
} catch (error) {
151-
throw new Error(`Error validating CSV format: ${error}`)
152-
}
153-
}
15495
}

packages/sources/ftse-sftp/src/parsing/interfaces.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@ export interface CSVParser {
88
* @returns Promise of parsed data
99
*/
1010
parse(csvContent: string): Promise<ParsedData[]>
11-
12-
/**
13-
* Validate the CSV format
14-
* @param csvContent - Raw CSV content as string
15-
* @returns boolean indicating if the format is valid
16-
*/
17-
validateFormat(csvContent: string): boolean
1811
}
1912

2013
/**

packages/sources/ftse-sftp/src/parsing/russell.ts

Lines changed: 20 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ParsedData } from './interfaces'
33

44
// Column indices for Russell CSV format
55
const INDEX_NAME_COLUMN = 0
6-
const VALUE_COLUMN = 4
6+
const CLOSE_VALUE_COLUMN = 4
7+
const FIRST_DATA_ROW = 7
78

89
/**
910
* Specific data structure for Russell Daily Values data
@@ -32,104 +33,33 @@ export class RussellDailyValuesParser extends BaseCSVParser {
3233
}
3334

3435
async parse(csvContent: string): Promise<RussellDailyValuesData[]> {
35-
const results: RussellDailyValuesData[] = []
36-
37-
if (!this.validateFormat(csvContent)) {
38-
throw new Error('Invalid CSV format for Russell data')
39-
}
40-
4136
// Russell data starts after the header rows, which vary in position
4237
// Parse the entire CSV and find Russell rows dynamically
4338
const parsed = this.parseCSV(csvContent, {
39+
from_line: FIRST_DATA_ROW, // Start parsing from line 7 (includes header)
4440
relax_column_count: true, // Allow rows with different number of columns
4541
})
4642

47-
for (const row of parsed) {
48-
if (!row || row.length < 5) {
49-
// Need at least 5 columns for index name and close value
50-
continue // Skip rows with insufficient fields
51-
}
52-
53-
// Skip empty rows (CSV contains separator rows with multiple empty fields)
54-
const hasContent = row.some((field: any) => field && String(field).trim() !== '')
55-
if (!hasContent) {
56-
continue // Skip empty rows
57-
}
58-
59-
const indexName = this.convertValue(row[INDEX_NAME_COLUMN], 'string') as string
60-
61-
// Only process rows that start with "Russell" and contain the ® symbol
62-
if (
63-
!indexName ||
64-
!indexName.includes('Russell') ||
65-
(!indexName.includes('®') && !indexName.includes('�'))
66-
) {
67-
continue
68-
}
69-
70-
const data: RussellDailyValuesData = {
71-
indexName,
72-
close: this.convertValue(row[VALUE_COLUMN], 'number') as number | null,
73-
}
74-
75-
// Filter by instrument if specified
76-
if (this.instrument) {
77-
// Normalize both strings for comparison (remove special characters and extra spaces)
78-
const normalizeString = (str: string) =>
79-
str.replace(/[®]/g, '').replace(/\s+/g, ' ').trim()
80-
const normalizedIndexName = normalizeString(indexName)
81-
const normalizedInstrument = normalizeString(this.instrument)
43+
const results: RussellDailyValuesData[] = parsed
44+
.filter((row) => row.length > CLOSE_VALUE_COLUMN) // Keep rows with enough columns
45+
.map((row) => ({
46+
row,
47+
indexName: this.convertValue(row[INDEX_NAME_COLUMN], 'string') as string,
48+
}))
49+
.filter(({ indexName }) => indexName && indexName === this.instrument) // Only process rows that match the instrument
50+
.map(
51+
({ row, indexName }): RussellDailyValuesData => ({
52+
indexName,
53+
close: this.convertValue(row[CLOSE_VALUE_COLUMN], 'number') as number | null,
54+
}),
55+
)
8256

83-
if (normalizedIndexName === normalizedInstrument) {
84-
results.push(data)
85-
}
86-
} else {
87-
results.push(data)
88-
}
57+
if (results.length === 0) {
58+
throw new Error('No matching Russell index records found')
59+
} else if (results.length > 1) {
60+
throw new Error('Multiple matching Russell index records found, expected only one')
8961
}
9062

9163
return results
9264
}
93-
94-
/**
95-
* Validate that the CSV contains Russell index data
96-
*/
97-
validateFormat(csvContent: string): boolean {
98-
if (!csvContent || csvContent.trim().length === 0) {
99-
return false
100-
}
101-
102-
try {
103-
// Parse the entire CSV with relaxed column count to find Russell data
104-
const parsed = this.parseCSV(csvContent, {
105-
relax_column_count: true,
106-
})
107-
108-
if (!parsed || parsed.length === 0) {
109-
console.error('No data rows found in CSV for Russell validation')
110-
return false
111-
}
112-
113-
// Check if any row contains valid Russell index data
114-
const hasValidRussellData = parsed.some(
115-
(row) =>
116-
row &&
117-
row.length >= 5 && // Must have at least 5 columns for index name and close value
118-
row[INDEX_NAME_COLUMN] &&
119-
String(row[INDEX_NAME_COLUMN]).includes('Russell') &&
120-
(String(row[INDEX_NAME_COLUMN]).includes('®') ||
121-
String(row[INDEX_NAME_COLUMN]).includes('�')),
122-
)
123-
124-
if (!hasValidRussellData) {
125-
console.error('No valid Russell index data found in CSV validation')
126-
return false
127-
}
128-
129-
return true
130-
} catch (error) {
131-
console.error('Error during Russell CSV validation:', error)
132-
return false
133-
}
134-
}
13565
}

0 commit comments

Comments
 (0)