"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ddfCsvReader = void 0;
const includes = require("lodash.includes");
const isEmpty = require("lodash.isempty");
const stripBom = require("strip-bom");
const resource_selection_optimizer_1 = require("./resource-selection-optimizer");
const ddfcsv_error_1 = require("./ddfcsv-error");
const ddf_query_validator_1 = require("ddf-query-validator");
const Papa = require("papaparse");
const d3_time_format_1 = require("d3-time-format");
const isValidNumeric = val => typeof val !== 'number' && !val ? false : true;
function ddfCsvReader(logger) {
    const internalConcepts = [
        { concept: 'concept', concept_type: 'string', domain: null },
        { concept: 'concept_type', concept_type: 'string', domain: null }
    ];
    const operators = new Map([
        ['$and', (row, predicates) => predicates.every(p => applyFilterRow(row, p))],
        ['$or', (row, predicates) => predicates.some(p => applyFilterRow(row, p))],
        ['$not', (row, predicate) => !applyFilterRow(row, predicate)],
        ['$nor', (row, predicates) => !predicates.some(p => applyFilterRow(row, p))],
        ['$eq', (rowValue, filterValue) => rowValue == filterValue],
        ['$ne', (rowValue, filterValue) => rowValue != filterValue],
        ['$gt', (rowValue, filterValue) => isValidNumeric(rowValue) && rowValue > filterValue],
        ['$gte', (rowValue, filterValue) => isValidNumeric(rowValue) && rowValue >= filterValue],
        ['$lt', (rowValue, filterValue) => isValidNumeric(rowValue) && rowValue < filterValue],
        ['$lte', (rowValue, filterValue) => isValidNumeric(rowValue) && rowValue <= filterValue],
        ['$in', (rowValue, filterValue) => filterValue.has(rowValue)],
        ['$nin', (rowValue, filterValue) => !filterValue.has(rowValue)],
    ]);
    const keyValueLookup = new Map();
    const resourcesLookup = new Map();
    let optimalFilesSet = [];
    let datapackage;
    let datapackagePromise;
    let datasetWithConstraints = false;
    function loadDataPackage(baseOptions) {
        const datapackagePath = (0, ddf_query_validator_1.getFilePath)(baseOptions.basePath);
        const { debug, error } = baseOptions.diagnostic.prepareDiagnosticFor('loadDataPackage');
        return new Promise((resolve, reject) => {
            if (datapackage) {
                return resolve(datapackage);
            }
            baseOptions.fileReader.readText(datapackagePath, (err, data) => {
                if (err) {
                    error('file reading', err);
                    return reject(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.FILE_READING_ERROR, err, datapackagePath));
                }
                try {
                    datapackage = JSON.parse(stripBom(data));
                    optimalFilesSet = [];
                    buildResourcesLookup(datapackage);
                    buildKeyValueLookup(datapackage);
                }
                catch (parseErr) {
                    error('json file parsing', parseErr);
                    return reject(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.JSON_PARSING_ERROR, parseErr.message, datapackagePath));
                }
                debug('datapackage content is ready');
                resolve(datapackage);
            });
        });
    }
    async function loadConcepts(queryParam, options) {
        const { error } = options.diagnostic.prepareDiagnosticFor('loadConcepts');
        setConceptsLookup(internalConcepts, options);
        const conceptQuery = {
            select: { key: ['concept'], value: ['concept_type', 'domain'] },
            from: 'concepts'
        };
        let result;
        try {
            const concepts = await queryData(conceptQuery, options);
            buildConceptsLookup(concepts, options);
            result = await reparseConcepts(options);
        }
        catch (err) {
            error('concepts processing', err);
            throw err;
        }
        return result;
    }
    function buildConceptsLookup(concepts, options) {
        const entitySetMembershipConcepts = concepts
            .filter(concept => concept.concept_type === 'entity_set')
            .map(concept => ({
            concept: 'is--' + concept.concept,
            concept_type: 'boolean',
            domain: null
        }));
        concepts = concepts
            .concat(entitySetMembershipConcepts)
            .concat(internalConcepts);
        setConceptsLookup(concepts, options);
    }
    function reparseConcepts({ conceptsLookup }) {
        const parsingFunctions = new Map([
            ['boolean', (str) => str === 'true' || str === 'TRUE'],
            ['measure', (str) => parseFloat(str)]
        ]);
        const resources = getResources(['concept']);
        const resourceUpdates = [...resources].map(resource => {
            return resource.data.then(response => {
                const resourceConcepts = Object.keys(response.data[0]);
                const parsingConcepts = new Map();
                resourceConcepts.forEach(concept => {
                    const type = conceptsLookup.get(concept).concept_type;
                    const fn = parsingFunctions.get(type);
                    if (fn) {
                        parsingConcepts.set(concept, fn);
                    }
                });
                return response.data.forEach(row => {
                    for (const [concept, parseFn] of parsingConcepts) {
                        row[concept] = parseFn(row[concept]);
                    }
                });
            });
        });
        return Promise.all(resourceUpdates);
    }
    function setConceptsLookup(concepts, options) {
        options.conceptsLookup.clear();
        concepts.forEach(row => options.conceptsLookup.set(row.concept, row));
    }
    async function query(queryParam, _baseOptions) {
        const baseOptions = Object.assign({}, _baseOptions);
        const { warning, error } = baseOptions.diagnostic.prepareDiagnosticFor('query');
        let data;
        try {
            await (0, ddf_query_validator_1.validateQueryStructure)(queryParam, baseOptions);
            baseOptions.datapackage = await (datapackagePromise || (datapackagePromise = loadDataPackage(baseOptions)));
            baseOptions.resourcesLookup = resourcesLookup;
            await loadConcepts(queryParam, baseOptions);
            await (0, ddf_query_validator_1.validateQueryDefinitions)(queryParam, baseOptions);
            if ((0, ddf_query_validator_1.isSchemaQuery)(queryParam)) {
                data = await querySchema(queryParam, baseOptions);
            }
            else {
                const appropriatePlugin = datasetWithConstraints && (0, resource_selection_optimizer_1.getAppropriatePlugin)(this, queryParam, baseOptions);
                optimalFilesSet = [];
                if (appropriatePlugin) {
                    const files = await appropriatePlugin.getRecommendedFilesSet();
                    optimalFilesSet = files;
                    queryParam.optimalFilesSet = [].concat(files, queryParam.optimalFilesSet);
                    warning('get custom optimal files list by a plugin', optimalFilesSet);
                }
                data = await queryData(queryParam, baseOptions);
            }
        }
        catch (err) {
            error('general query error', err);
            throw err;
        }
        return data;
    }
    function queryData(queryParam, _options) {
        const options = Object.assign({}, _options);
        const { debug } = options.diagnostic.prepareDiagnosticFor('queryData');
        const { select: { key = [], value = [] }, from = '', where = {}, join = {}, order_by = [], language } = queryParam;
        const select = { key, value };
        debug('start all data loading', queryParam);
        const projection = new Set(select.key.concat(select.value));
        const filterFields = getFilterFields(where).filter(field => from === 'entities' || !projection.has(field));
        const resourcesPromise = loadResources(select.key, [...select.value, ...filterFields], language, options, queryParam);
        const joinsPromise = getJoinFilters(join, queryParam, options);
        const entitySetFilterPromise = getEntitySetFilter(select.key, queryParam, options);
        return Promise.all([resourcesPromise, entitySetFilterPromise, joinsPromise])
            .then(([resourceResponses, entitySetFilter, joinFilters]) => {
            debug('finish all data loading', queryParam);
            const whereResolved = processWhere(where, joinFilters);
            const filter = mergeFilters(entitySetFilter, whereResolved);
            debug('dataTables processing', queryParam);
            const dataTables = resourceResponses
                .map(response => processResourceResponse(response, select, filterFields, options));
            debug('queryResult processing', queryParam);
            const queryResult = joinData(select.key, 'overwrite', ...dataTables)
                .filter(row => applyFilterRow(row, filter))
                .map(row => fillMissingValues(row, projection))
                .map(row => projectRow(row, projection));
            debug('result ordering', queryParam);
            orderData(queryResult, order_by);
            debug('final result is ready', queryParam);
            return parseTime(queryResult, options);
        });
    }
    function parseTime(result, options) {
        const conceptsLookup = options.conceptsLookup;
        const concepts = Object.keys(result[0] || {});
        const timeConcepts = concepts.map(c => conceptsLookup.get(c) || {}).filter(co => co.concept_type == 'time');
        timeConcepts.forEach(({ concept }) => {
            const parse = getTimeParser(concept, options);
            result.forEach(row => {
                row[concept] = parse(row[concept]);
            });
        });
        return result;
    }
    function getTimeParser(concept, options) {
        const { error } = options.diagnostic.prepareDiagnosticFor('queryData');
        const parsers = {
            year: (0, d3_time_format_1.utcParse)('%Y'),
            month: (0, d3_time_format_1.utcParse)('%Y-%m'),
            day: (0, d3_time_format_1.utcParse)('%Y%m%d'),
            hour: (0, d3_time_format_1.utcParse)('%Y%m%dt%H'),
            minute: (0, d3_time_format_1.utcParse)('%Y%m%dt%H%M'),
            second: (0, d3_time_format_1.utcParse)('%Y%m%dt%H%M%S'),
            week: (0, d3_time_format_1.utcParse)('%Yw%V'),
            quarter: (0, d3_time_format_1.utcParse)('%Yq%q')
        };
        function tryParse(str) {
            for (const i in parsers) {
                const dateObject = parsers[i](str);
                if (dateObject) {
                    return dateObject;
                }
            }
            error('Could not parse time string: ' + str);
            return null;
        }
        if (concept == 'time') {
            return tryParse;
        }
        if (!parsers[concept]) {
            error('No time parser found for time concept: ' + concept);
            return str => str;
        }
        return parsers[concept];
    }
    function orderData(data, orderBy = []) {
        if (orderBy.length === 0) {
            return;
        }
        const orderNormalized = orderBy.map(orderPart => {
            if (typeof orderPart === 'string') {
                return { concept: orderPart, direction: 1 };
            }
            else {
                const concept = Object.keys(orderPart)[0];
                const direction = (orderPart[concept] === 'asc' ? 1 : -1);
                return { concept, direction };
            }
        });
        const n = orderNormalized.length;
        data.sort((a, b) => {
            for (let i = 0; i < n; i++) {
                const order = orderNormalized[i];
                if (a[order.concept] < b[order.concept]) {
                    return -1 * order.direction;
                }
                else if (a[order.concept] > b[order.concept]) {
                    return 1 * order.direction;
                }
            }
            return 0;
        });
    }
    function processWhere(where, joinFilters) {
        const result = {};
        for (const field in where) {
            const fieldValue = where[field];
            if (includes(['$and', '$or', '$nor'], field)) {
                result[field] = fieldValue.map(subFilter => processWhere(subFilter, joinFilters));
            }
            else if (field === '$in' || field === '$nin') {
                result[field] = new Set(fieldValue);
            }
            else if (typeof joinFilters[fieldValue] !== 'undefined') {
                Object.assign(result, joinFilters[fieldValue]);
            }
            else if (typeof fieldValue === 'object') {
                result[field] = processWhere(fieldValue, joinFilters);
            }
            else {
                result[field] = fieldValue;
            }
        }
        return result;
    }
    function patchFilterForOrClause(filter) {
        function processFilter(where) {
            const whereKeys = Object.keys(where);
            for (const key of whereKeys) {
                if (key == "$or") {
                    where[key] = where[key].reduce((res, value) => {
                        const valueKeys = Object.keys(value);
                        if (valueKeys.length > 1) {
                            for (const key of valueKeys) {
                                res.push({ [key]: value[key] });
                            }
                        }
                        else {
                            res.push(value);
                        }
                        return res;
                    }, []);
                }
            }
            return where;
        }
        return processFilter(filter);
    }
    function mergeFilters(...filters) {
        return filters.reduce((a, b) => {
            if (!isEmpty(b)) {
                patchFilterForOrClause(b);
                a.$and.push(b);
            }
            return a;
        }, { $and: [] });
    }
    function querySchema(queryParam, baseOptions) {
        const { debug, error } = baseOptions.diagnostic.prepareDiagnosticFor('query');
        const getSchemaFromCollection = collectionPar => {
            debug(`get schema for collection ${collectionPar}`);
            return baseOptions.datapackage.ddfSchema[collectionPar].map(({ primaryKey, value }) => ({ key: primaryKey, value }));
        };
        const collection = queryParam.from.split('.')[0];
        if (baseOptions.datapackage.ddfSchema[collection]) {
            return getSchemaFromCollection(collection);
        }
        else if (collection === '*') {
            return Object.keys(baseOptions.datapackage.ddfSchema)
                .map(getSchemaFromCollection)
                .reduce((a, b) => a.concat(b));
        }
        else {
            const message = `No valid collection (${collection}) for schema query`;
            error(message);
            throwError(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.DDF_ERROR, message));
        }
    }
    function fillMissingValues(row, projection) {
        for (const field of projection) {
            if (typeof row[field] === 'undefined') {
                row[field] = null;
            }
        }
        return row;
    }
    function applyFilterRow(row, filter) {
        return Object.keys(filter).every(filterKey => {
            const operator = operators.get(filterKey);
            if (operator) {
                return operator(row, filter[filterKey]);
            }
            else if (typeof filter[filterKey] !== 'object') {
                return operators.get('$eq')(row[filterKey], filter[filterKey]);
            }
            else {
                return applyFilterRow(row[filterKey], filter[filterKey]);
            }
        });
    }
    function getJoinFilters(join, queryParam, options) {
        return Promise.all(Object.keys(join).map(joinID => getJoinFilter(joinID, join[joinID], queryParam, options)))
            .then(results => results.reduce(mergeObjects, {}));
    }
    function mergeObjects(a, b) {
        return Object.assign(a, b);
    }
    function getJoinFilter(joinID, join, queryParam, options) {
        if (options.conceptsLookup.get(join.key).concept_type === 'time') {
            return Promise.resolve({ [joinID]: join.where });
        }
        else {
            return queryData({
                select: { key: [join.key] },
                where: join.where,
                from: options.conceptsLookup.has(join.key) ? 'entities' : 'concepts'
            }, Object.assign({ joinID }, options))
                .then(result => ({
                [joinID]: {
                    [join.key]: {
                        $in: new Set(result.map(row => row[join.key]))
                    }
                }
            }));
        }
    }
    function getFilterFields(filter) {
        const fields = [];
        for (const field in filter) {
            if (includes(['$and', '$or', '$not', '$nor'], field)) {
                filter[field].map(getFilterFields).forEach(subFields => fields.push(...subFields));
            }
            else {
                fields.push(field);
            }
        }
        return [...new Set(fields)];
    }
    function filterConceptsByType(conceptTypes, queryKey, options) {
        const conceptStrings = queryKey || Array.from(options.conceptsLookup.keys());
        const concepts = [];
        for (const conceptString of conceptStrings) {
            const concept = options.conceptsLookup.get(conceptString);
            if (includes(conceptTypes, concept.concept_type)) {
                concepts.push(concept);
            }
        }
        return concepts;
    }
    function getEntityConceptRenameMap(queryKey, resourceKey, options) {
        const resourceKeySet = new Set(resourceKey);
        const entityConceptTypes = ['entity_set', 'entity_domain'];
        const queryEntityConcepts = filterConceptsByType(entityConceptTypes, queryKey, options);
        if (queryEntityConcepts.length === 0) {
            return new Map();
        }
        const allEntityConcepts = filterConceptsByType(entityConceptTypes, null, options);
        return queryEntityConcepts
            .map(concept => allEntityConcepts
            .filter(lookupConcept => {
            if (concept.concept_type === 'entity_set') {
                return resourceKeySet.has(lookupConcept.concept) &&
                    lookupConcept.concept !== concept.concept &&
                    (lookupConcept.domain === concept.domain ||
                        lookupConcept.concept === concept.domain);
            }
            else {
                return resourceKeySet.has(lookupConcept.concept) &&
                    lookupConcept.concept !== concept.concept &&
                    lookupConcept.domain === concept.concept;
            }
        })
            .reduce((map, aliasConcept) => map.set(aliasConcept.concept, concept.concept), new Map())).reduce((mapA, mapB) => new Map([...mapA, ...mapB]), new Map());
    }
    function getEntitySetFilter(conceptStrings, queryParam, options) {
        const promises = filterConceptsByType(['entity_set'], conceptStrings, options)
            .map(concept => queryData({
            select: { key: [concept.domain], value: ['is--' + concept.concept] },
            from: 'entities'
        }, Object.assign({}, options))
            .then(result => ({
            [concept.concept]: {
                $in: new Set(result
                    .filter(row => row['is--' + concept.concept])
                    .map(row => row[concept.domain]))
            }
        })));
        return Promise.all(promises).then(results => {
            return results.reduce((a, b) => Object.assign(a, b), {});
        });
    }
    function getResources(key, value) {
        if (!value || value.length === 0 || key[0] === value) {
            return new Set([...keyValueLookup
                    .get(createKeyString(key))
                    .values()
            ].reduce((a, b) => a.concat(b)));
        }
        if (Array.isArray(value)) {
            return value
                .map(singleValue => getResources(key, singleValue))
                .reduce((resultSet, resources) => new Set([...resultSet, ...resources]), new Set());
        }
        let oneKeyOneValueResourcesArray = keyValueLookup
            .get(createKeyString(key))
            .get(value);
        if (oneKeyOneValueResourcesArray) {
            oneKeyOneValueResourcesArray = oneKeyOneValueResourcesArray
                .filter(v => isEmpty(optimalFilesSet) || includes(optimalFilesSet, v.path));
        }
        return new Set(oneKeyOneValueResourcesArray);
    }
    function processResourceResponse(response, select, filterFields, options) {
        const resourcePK = response.resource.schema.primaryKey;
        const resourceProjection = new Set([...resourcePK, ...select.value, ...filterFields]);
        const renameMap = getEntityConceptRenameMap(select.key, resourcePK, options);
        return response.data
            .map(row => projectRow(row, resourceProjection))
            .map(row => renameHeaderRow(row, renameMap));
    }
    function loadResources(key, value, language, options, queryParam) {
        const { debug } = options.diagnostic.prepareDiagnosticFor('loadResource');
        const resources = getResources(key, value);
        debug('resources list by query', { queryParam, resources: [...resources] });
        return Promise.all([...resources].map(resource => loadResource(resource, language, options)));
    }
    function projectRow(row, projectionSet) {
        const result = {};
        for (const concept of Object.keys(row)) {
            if (projectionSet.has(concept)) {
                result[concept] = row[concept];
            }
        }
        return result;
    }
    function renameHeaderRow(row, renameMap) {
        const result = {};
        for (const concept of Object.keys(row)) {
            result[renameMap.get(concept) || concept] = row[concept];
        }
        return result;
    }
    function joinData(key, joinMode, ...data) {
        if (data.length === 1) {
            return data[0];
        }
        const canonicalKey = key.slice(0).sort();
        const dataMap = data.reduce((result, dataPar) => {
            dataPar.forEach(row => {
                const keyString = canonicalKey.map(concept => row[concept]).join(',');
                if (result.has(keyString)) {
                    const resultRow = result.get(keyString);
                    joinRow(resultRow, row, joinMode);
                }
                else {
                    result.set(keyString, Object.assign({}, row));
                }
            });
            return result;
        }, new Map());
        return [...dataMap.values()];
    }
    function joinRow(resultRow, sourceRow, mode) {
        switch (mode) {
            case 'overwrite':
                Object.assign(resultRow, sourceRow);
                break;
            case 'translation':
                for (const concept in sourceRow) {
                    if (sourceRow[concept] !== '') {
                        resultRow[concept] = sourceRow[concept];
                    }
                }
                break;
            case 'overwriteWithError':
                for (const concept in sourceRow) {
                    if (resultRow[concept] !== undefined && resultRow[concept] !== sourceRow[concept]) {
                        const sourceRowStr = JSON.stringify(sourceRow);
                        const resultRowStr = JSON.stringify(resultRow);
                        const errStr = `JOIN Error: two resources have different data for "${concept}": ${sourceRowStr},${resultRowStr}`;
                        throwError(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.DDF_ERROR, errStr));
                    }
                    else {
                        resultRow[concept] = sourceRow[concept];
                    }
                }
                break;
        }
    }
    function throwError(error) {
        const currentLogger = logger || console;
        currentLogger.error(error.message);
        throw error;
    }
    function createKeyString(key, row = false) {
        const canonicalKey = key.slice(0).sort();
        if (!row) {
            return canonicalKey.join(',');
        }
        else {
            return canonicalKey.map(concept => row[concept]).join(',');
        }
    }
    function loadResource(resource, language, options) {
        const { warning } = options.diagnostic.prepareDiagnosticFor('loadResource');
        const filePromises = [];
        if (typeof resource.data === 'undefined') {
            resource.data = loadFile(resource.path, options);
        }
        filePromises.push(resource.data);
        const languageValid = typeof language !== 'undefined' && includes(getLanguages(options), language);
        const languageLoaded = typeof resource.translations[language] !== 'undefined';
        if (languageValid) {
            if (!languageLoaded) {
                const translationPath = `lang/${language}/${resource.path}`;
                resource.translations[language] = loadFile(translationPath, options)
                    .catch(err => {
                    warning(`translation file ${translationPath}`, err);
                    return Promise.resolve({});
                });
            }
            filePromises.push(resource.translations[language]);
        }
        return Promise.all(filePromises).then(fileResponses => {
            const filesData = fileResponses.map(resp => resp.data || []);
            const primaryKey = resource.schema.primaryKey;
            const data = joinData(primaryKey, 'translation', ...filesData);
            return { data, resource };
        });
    }
    function getLanguages(options) {
        if (!options.datapackage.translations) {
            return [];
        }
        return options.datapackage.translations.map(lang => lang.id);
    }
    function loadFile(filePath, options) {
        const { debug, error } = options.diagnostic.prepareDiagnosticFor('loadFile');
        const fullFilePath = (0, ddf_query_validator_1.getFilePath)(options.basePath, filePath);
        debug(`start reading "${filePath}"`);
        return new Promise((resolve, reject) => {
            options.fileReader.readText(fullFilePath, (err, data) => {
                if (err) {
                    error(`fail "${filePath}" reading`, err);
                    return reject(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.FILE_READING_ERROR, err, fullFilePath));
                }
                Papa.parse(stripBom(data), {
                    header: true,
                    skipEmptyLines: true,
                    dynamicTyping: (headerName) => {
                        const concept = options.conceptsLookup.get(headerName) || {};
                        return !includes(['time', 'string'], concept.concept_type);
                    },
                    complete: result => {
                        debug(`finish reading "${filePath}"`);
                        resolve(result);
                    },
                    error: parseErr => {
                        error(`fail "${filePath}" parsing`, parseErr);
                        reject(new ddfcsv_error_1.DdfCsvError(ddfcsv_error_1.CSV_PARSING_ERROR, parseErr, filePath));
                    }
                });
            });
        });
    }
    function buildResourcesLookup(datapackagePar) {
        if (resourcesLookup.size > 0) {
            return resourcesLookup;
        }
        datapackagePar.resources.forEach(resource => {
            if (!Array.isArray(resource.schema.primaryKey)) {
                resource.schema.primaryKey = [resource.schema.primaryKey];
            }
            const constraints = resource.schema.fields.reduce((result, field) => {
                var _a;
                if ((_a = field.constraints) === null || _a === void 0 ? void 0 : _a.enum) {
                    if (!datasetWithConstraints)
                        datasetWithConstraints = true;
                    result[field.name] = field.constraints.enum.map(e => +e || e);
                }
                return result;
            }, {});
            resource.constraints = constraints;
            resource.translations = {};
            resourcesLookup.set(resource.name, resource);
        });
        return resourcesLookup;
    }
    function buildKeyValueLookup(datapackagePar) {
        if (keyValueLookup.size > 0) {
            return keyValueLookup;
        }
        for (const collection in datapackagePar.ddfSchema) {
            datapackagePar.ddfSchema[collection].map(kvPair => {
                const key = createKeyString(kvPair.primaryKey);
                const resources = kvPair.resources.map(resourceName => resourcesLookup.get(resourceName));
                if (keyValueLookup.has(key)) {
                    keyValueLookup.get(key).set(kvPair.value, resources);
                }
                else {
                    keyValueLookup.set(key, new Map([[kvPair.value, resources]]));
                }
            });
        }
        return keyValueLookup;
    }
    return {
        query,
        queryData,
        loadFile
    };
}
exports.ddfCsvReader = ddfCsvReader;
//# sourceMappingURL=ddf-csv.js.map