SCRAPE-5: Implement convertDataTypes() with type parsing helpers (#10)
Co-authored-by: Stephen Minakian <stephenminakian@gmail.com> Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
This commit is contained in:
@ -198,10 +198,189 @@ function parseUnits(html, logger) {
|
||||
return deduplicated;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Data Type Conversion Helpers
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Parse string to integer, return null for invalid
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {number|null} Parsed integer or null
|
||||
*/
|
||||
function parseInteger(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) || !Number.isFinite(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string to positive integer, return null for 0 or invalid.
|
||||
* Used for fields like area where 0 means "not available".
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {number|null} Parsed positive integer or null
|
||||
*/
|
||||
function parsePositiveInteger(value) {
|
||||
const parsed = parseInteger(value);
|
||||
return parsed === 0 ? null : parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse value that could be integer or string identifier.
|
||||
* Returns integer if cleanly parseable, otherwise trimmed string.
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {number|string|null} Parsed integer, trimmed string, or null
|
||||
*/
|
||||
function parseIntegerOrString(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseInt(value, 10);
|
||||
// If it parses cleanly to an integer, return integer
|
||||
if (!Number.isNaN(parsed) && String(parsed) === String(value).trim()) {
|
||||
return parsed;
|
||||
}
|
||||
// Otherwise return as trimmed string
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse price, stripping non-numeric characters.
|
||||
* Handles "$1,234" format and "Call for pricing" text.
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {number|null} Parsed price or null
|
||||
*/
|
||||
function parsePrice(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for "Call for pricing" or similar text
|
||||
if (typeof value === 'string' && /call|contact|inquire/i.test(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Strip non-numeric characters except decimal point
|
||||
const cleaned = String(value).replace(/[^0-9.]/g, '');
|
||||
const parsed = parseInt(cleaned, 10);
|
||||
|
||||
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Negative prices are invalid
|
||||
if (parsed < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse boolean from string.
|
||||
* Handles "true"/"false"/"1"/"0" and actual boolean values.
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {boolean|null} Parsed boolean or null
|
||||
*/
|
||||
function parseBoolean(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
const str = String(value).toLowerCase().trim();
|
||||
if (str === 'true' || str === '1') return true;
|
||||
if (str === 'false' || str === '0') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse float value.
|
||||
* Named parseFloatValue to avoid shadowing the global parseFloat.
|
||||
* @param {*} value - Value to parse
|
||||
* @returns {number|null} Parsed float or null
|
||||
*/
|
||||
function parseFloatValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isNaN(parsed) || !Number.isFinite(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim string, return null for empty.
|
||||
* Converts non-string values to string before trimming.
|
||||
* @param {*} value - Value to trim
|
||||
* @returns {string|null} Trimmed string or null
|
||||
*/
|
||||
function trimString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = String(value).trim();
|
||||
return trimmed === '' ? null : trimmed;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main Data Type Conversion Function
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Convert unit data types from strings to proper types.
|
||||
* Applies the appropriate parser to each field based on its expected type.
|
||||
* @param {Object} unit - Raw unit object with string values
|
||||
* @returns {Object} Unit object with converted types
|
||||
*/
|
||||
function convertDataTypes(unit) {
|
||||
return {
|
||||
// Integer fields
|
||||
id: parseInteger(unit.id),
|
||||
unit_id: parseIntegerOrString(unit.unit_id),
|
||||
floor: parseInteger(unit.floor),
|
||||
area: parsePositiveInteger(unit.area),
|
||||
bed_count: parseInteger(unit.bed_count),
|
||||
plan_id: parseInteger(unit.plan_id),
|
||||
asset: parseInteger(unit.asset),
|
||||
date_available: parseInteger(unit.date_available),
|
||||
|
||||
// Float fields
|
||||
bath_count: parseFloatValue(unit.bath_count),
|
||||
|
||||
// Price field (special handling)
|
||||
price: parsePrice(unit.price),
|
||||
|
||||
// Boolean fields
|
||||
available: parseBoolean(unit.available),
|
||||
unavailable: parseBoolean(unit.unavailable),
|
||||
|
||||
// String fields (trim and preserve)
|
||||
unit_code: trimString(unit.unit_code),
|
||||
plan_name: trimString(unit.plan_name),
|
||||
soonest: trimString(unit.soonest),
|
||||
obj_type: trimString(unit.obj_type),
|
||||
community: trimString(unit.community),
|
||||
href: trimString(unit.href),
|
||||
inventory_href: trimString(unit.inventory_href),
|
||||
image_url: trimString(unit.image_url),
|
||||
specials_content: trimString(unit.specials_content)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchPage,
|
||||
parseUnits,
|
||||
// Export for testing
|
||||
convertDataTypes,
|
||||
// Export helpers for testing
|
||||
parseInteger,
|
||||
parsePositiveInteger,
|
||||
parseIntegerOrString,
|
||||
parsePrice,
|
||||
parseBoolean,
|
||||
parseFloatValue,
|
||||
trimString,
|
||||
isRetryableError,
|
||||
sleep
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user