@benhawley7/fut-search
Version:
Module for searching for players in FIFA Ultimate Team CSV Data.
211 lines • 9.35 kB
JavaScript
;
/**
* @package fut-search
* @author Ben Hawley
* @file Contains class for streaming and searching FUT CSV data
* @copyright Ben Hawley 2020
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FUTSearch = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const parse_1 = require("@fast-csv/parse");
/**
* Class to search FUT CSV data for Players
* @class
*/
class FUTSearch {
/**
* Create an instance of FUT
* @param dataPath set where the CSV is located
*/
constructor(dataPath) {
/**
* Path to the CSV Data
* By default, we look three directories up -
* as this will take us to the project root from project/node_modules/@benhawley7/fut-search
* @private
*/
this._dataPath = path.join(__dirname, "../../../", "data", "FIFA20.csv");
if (dataPath) {
this.dataPath = dataPath;
}
}
/**
* Returns the path to the current CSV we are accessing
*/
get dataPath() {
return this._dataPath;
}
/**
* Sets the path to the CSV we wish to query, errors if file does not exist
* @param val where to look for our FUT CSV data
*/
set dataPath(val) {
const isCSV = val.endsWith(".csv");
if (isCSV === false) {
throw new Error("dataPath must point to a CSV file");
}
this._dataPath = val;
}
/**
* Parse a CSV record as a Player
* @param record the csv row
* @returns parsed Player
*/
static parsePlayer(record) {
const formattedRecord = {};
for (const [key, value] of Object.entries(record)) {
formattedRecord[key.toLowerCase()] = value;
}
return {
name: String(formattedRecord.name),
club: String(formattedRecord.club),
position: String(formattedRecord.position),
revision: String(formattedRecord.revision),
league: String(formattedRecord.league),
rating: parseInt(formattedRecord.rating, 10),
pace: parseInt(formattedRecord.pace, 10),
shooting: parseInt(formattedRecord.shooting, 10),
passing: parseInt(formattedRecord.passing, 10),
dribbling: parseInt(formattedRecord.dribbling, 10),
defending: parseInt(formattedRecord.defending, 10),
physicality: parseInt(formattedRecord.physicality, 10)
};
}
/**
* Compare an incomplete player record to a complete player
* @param partial an incomplete player
* @param player a complete player
* @returns are they are a potential match?
*/
static playerMatch(partial, player) {
for (const [key, value] of Object.entries(partial)) {
// For numbers, we simply check the values match
if (typeof value === "number") {
if (partial[key] !== player[key]) {
return false;
}
}
else {
const partialValueLowerCase = String(partial[key])
.toLowerCase()
.normalize("NFKD")
.replace(/[^\w\s.-_\/]/g, '');
const playerValueLowerCase = String(player[key])
.toLowerCase()
.normalize("NFKD")
.replace(/[^\w\s.-_\/]/g, '');
if (playerValueLowerCase.includes(partialValueLowerCase) === false) {
return false;
}
}
}
return true;
}
/**
* Finding matching players for a batch of partial players
* @param partialPlayers array of partials to find
* @param options flags for search
* @param options.firstMatchOnly if true we return only the first match for each partial
* @returns array of matching players for each partial supplied
*/
listPlayersBatch(partialPlayers, options = { firstMatchOnly: false }) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res, rej) => {
// We store an array of each partial containing its player matches
const matchingPlayers = partialPlayers.map(() => []);
// To find a the matching player, we need to read the CSV. These CSVs have about 15,000
// rows, so loading the entire CSV into memory doesn't seem sensible.
// Instead, we can read the CSV as a stream and check each row as it comes in
const csvReadStream = fs.createReadStream(this.dataPath)
.on("error", (error) => {
rej(error);
});
// The parse and match CSV Parser Stream will have the CSV stream piped into it, for each row of data
// it will parse the data into our Player interface and will check if it is a match on our
// input name and club. Upon completion, it will resolve the matching player, or reject with an error.
const parseAndMatch = parse_1.parse({ headers: true })
.on('error', error => rej(new Error(`Error parsing CSV: ${error.message}`)))
.on('data', row => {
// With the first match only flag, we resolve early if we have one match for every player
const canEarlyResolve = options.firstMatchOnly && matchingPlayers.every(playerMatch => playerMatch.length > 0);
if (canEarlyResolve) {
// Resolve the early matches
res(matchingPlayers);
// Destroy the streams
csvReadStream.destroy();
parseAndMatch.destroy();
return;
}
// Parse the current row as a complete player
const player = FUTSearch.parsePlayer(row);
for (const [index, partial] of partialPlayers.entries()) {
const isMatch = FUTSearch.playerMatch(partial, player);
const partialComplete = options.firstMatchOnly && matchingPlayers[index].length > 0;
if (isMatch === true && partialComplete === false) {
matchingPlayers[index].push(player);
}
}
})
.on('end', () => {
res(matchingPlayers);
});
// Pipe the CSV into the parseAndMatch CSV Parser Stream
csvReadStream.pipe(parseAndMatch);
});
});
}
/**
* Finding matching players for a supplied partial player
* @param playerDetails partial player stats
* @returns array of matching players
*/
listPlayers(playerDetails = {}) {
return __awaiter(this, void 0, void 0, function* () {
const [players] = yield this.listPlayersBatch([playerDetails]);
return players;
});
}
/**
* Find a player with the supplied stats
* @param playerDetails partial player stats
* @returns player
*/
findPlayer(playerDetails) {
return __awaiter(this, void 0, void 0, function* () {
const [[player]] = yield this.listPlayersBatch([playerDetails], { firstMatchOnly: true });
return player;
});
}
}
exports.FUTSearch = FUTSearch;
//# sourceMappingURL=index.js.map