'use strict';
const assert = require('assert');
const util = require('util');
const _ = require('lodash');
/**
* @class
* @classdesc Gathers all the methods in the module
* @alias PublicationsManager
*/
module.exports = class PublicationsManager {
/**
* Class constructor. Receives only two parameters, the raw publications and author data.
* @constructor
* @param {Object} authorData The raw data of the author received from publications, in [this format](assets/author-data.json)
* @param {Object} publicationsData The raw data received from publications, in [this format](assets/publications-data.json)
* @throws {AssertionError} Throws an error if publicationsData is not defined
*/
constructor(authorData, publications) {
assert.notStrictEqual(util.isNullOrUndefined(publications), true, "publications can't be Null or Undefined");
assert.strictEqual(publications instanceof Object, true, "publications must be an object");
if (!util.isNullOrUndefined(authorData)) {
assert.strictEqual(authorData instanceof Object, true, "authorData must be an object");
}
this.publications = publications;
this.authorData = authorData;
}
/**
* Returns an array with the publications found in the input range, grouped by year. The output array has the following format:
* ```
* [
* {"year": 2014, "publications": [...{publicationsRawDataFragment}]},
* {"year": 2015, "publications": [...{publicationsRawDataFragment}]},
* {"year": 2016, "publications": [...{publicationsRawDataFragment}]},
* ]
* ```
* Each `publicationsRawDataFragment` contains all the data from each publication
* @method getPublicationsPerYear
* @memberof PublicationsManager
* @param {Number} from The year from which start to look for publications
* @param {Number} to The year to end looking for publications
* @returns {Array} Array with the publications found, grouped by year
* @instance
* @throws {AssertionError} Throws an error if the range is not valid (i.e. the starting year is greater than the ending year)
*/
getPublicationsPerYear(from, to) {
assert(typeof (from) === 'number' && typeof (to) === 'number', "Both of the years must be numbers");
assert(from <= to, "Starting year must be greater or equal than ending year");
// Initialize the resulting object with all the groups by year, empty
let publicationsObject = {};
_.range(from, to + 1).forEach((year) => {
publicationsObject[year] = [];
});
// For each publication
this.publications.forEach((publication => {
// Get its date
const publicationDate = this.__getPublicationDate(publication);
// If the publication is in the range passed as parameter
if (publicationDate
&& publicationDate.getFullYear() <= to
&& publicationDate.getFullYear() >= from) {
// Include the publication in its year group
publicationsObject[publicationDate.getFullYear()].push(publication);
}
}));
// Transform the object with format <year>: [publications] to an array
// of objects of publications grouped by year
const resultObject = Object.keys(publicationsObject).map(key => {
return { year: parseInt(key), publications: publicationsObject[key] };
});
return resultObject;
}
/**
* Returns an array with the publications found in the input range, ungrouped. The output array has the following format:
* ```
* [...{publicationsRawDataFragment}]
* ```
* Each `publicationsRawDataFragment` contains all the data from each publication
* @method getPublicationsInRange
* @memberof PublicationsManager
* @param {Number} from The year from which start to look for publications
* @param {Number} to The year to end looking for publications
* @returns {Array} Array with the publications found, ungrouped
* @throws {AssertionError} Throws an error if the range is not valid (i.e. the starting year is greater than the ending year)
* @instance
*/
getPublicationsInRange(from, to) {
// All the arguments assertions are made inside getPublicationsPerYear
const publicationsGrouped = this.getPublicationsPerYear(from, to);
// Map the JSON to an array of arrays
let ungrouped = publicationsGrouped.map(group => { return group.publications; });
// Flatten the array
ungrouped = [].concat.apply([], ungrouped);
return ungrouped;
}
/**
* Returns an array with every publication citation number, grouped by year. The output array has the following format:
* ```
* [...{
* "year": 2014,
* "cites": [...Number]
* }]
* ```
* The `cites` property contains an array representing the number of cites of each publication
* @method getCitesPerYear
* @memberof PublicationsManager
* @param {Number} from The year from which start to look for citations
* @param {Number} to The year to end looking for citations
* @returns {Array} Array with the citations found, grouped by year
* @throws {AssertionError} Throws an error if the range is not valid (i.e. the starting year is greater than the ending year)
* @instance
*/
getCitesPerYear(from, to) {
// All the arguments assertions are made inside getPublicationsPerYear
let publicationsGrouped = this.getPublicationsPerYear(from, to);
// Transform the array of publications of each year to an array of numbers, representing
// each publication cite number
publicationsGrouped = publicationsGrouped.map(group => {
return {
year: group.year,
cites: group['publications'].map(
publication => {
// If citedby-count is not present return 0 instead of undefined or NaN
return parseInt(publication['citedby-count']) || 0;
}
)
};
});
return publicationsGrouped;
}
/**
* Returns the last year in which an researcher published
* @method getLastPublicationYear
* @memberof PublicationsManager
* @returns {Number?} The last year in which the researcher published or null if there are no years
* @instance
*/
getLastPublicationYear() {
const years = this.publications.map(publication => {
const date = this.__getPublicationDate(publication);
return date ? date.getFullYear() : null;
}).filter(year => {
return year !== null;
});
// This prevents from returning -Infinity
if (years.length === 0) {
return null;
} else {
return Math.max.apply(Math, years);
}
}
/**
* Returns the array of publications scopus ids
* @method getScopusIds
* @memberof PublicationsManager
* @returns {Array} Array of the publications scopus ids
* @instance
*/
getScopusIds() {
return this.publications.map(p => p['dc:identifier'].split('SCOPUS_ID:')[1]);
}
/**
* Returns the SCOPUS ID of a publication
* @method getPublicationScopusId
* @memberof PublicationsManager
* @param {Object} rawPublication The raw publication from which you want to extract the SCOPUS_ID
* @returns {String} The publication SCOPUS ID
* @instance
*/
getPublicationScopusId(rawPublication) {
return rawPublication['dc:identifier'].split('SCOPUS_ID:')[1];
}
/**
* Returns a string with publication title of given scopus id
* @method getPublicationTitle
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {String} Publication title of the publication object
* @instance
*/
getPublicationTitle(scopusId) {
let publication = this.getPublication(scopusId);
return publication["dc:title"];
}
/**
* Returns a Date object with publication date of given scopus id
* @method getPublicationDate
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {Date} Publication date of the publication object
* @instance
*/
getPublicationDate(scopusId) {
let publication = this.getPublication(scopusId);
return this.__getPublicationDate(publication);
}
/**
* Returns a Date object with publication date of given publication
* @method getPublicationDateFromPublication
* @memberof PublicationsManager
* @param {Object} publication The publication object
* @returns {Date} Publication date of the publication object
* @instance
*/
getPublicationDateFromPublication(publication) {
this.__isPublication(publication);
return this.__getPublicationDate(publication);
}
/**
* Returns a string with publication DOI of given scopus id
* @method getPublicationDOI
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {String} Publication DOI of the publication object
* @instance
*/
getPublicationDOI(scopusId) {
let publication = this.getPublication(scopusId);
return publication["prism:doi"];
}
/**
* Returns an integer with maximun publication percentile of given scopus id
* @method getPublicationPercentile
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {String} Publication percentile of the publication object
* @instance
*/
getPublicationPercentile(scopusId) {
let publication = this.getPublication(scopusId);
return Number(publication["percentile"]);
}
/**
* Returns an object with the three fields about the publication category of given scopus id:
* ```
* {
* "aggregationType": "Journal",
* "subtype": "ar",
* "subtypeDescription": "Article"
* }
* ```
* @method getPublicationType
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of the publication
* @returns {Object} Three fields of publication category
* @instance
*/
getPublicationType(scopusId) {
let publication = this.getPublication(scopusId);
return {
"aggregationType": publication["prism:aggregationType"],
"subtype": publication["subtype"],
"subtypeDescription": publication["subtypeDescription"]
};
}
/**
* Returns a unique publication category given its scopus id
* @method getPublicationUniqueType
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of the publication
* @returns {String} The publication category. It can be 'article', 'conference' or 'other'
* @instance
*/
getPublicationUniqueType(scopusId) {
let publication = this.getPublication(scopusId);
return this.getPublicationUniqueTypeFromPublication(publication);
}
/**
* Returns a unique publication category given a publication object
* @method getPublicationUniqueTypeFromPublication
* @memberof PublicationsManager
* @param {Object} publication The publication object
* @returns {String} The publication category. It can be 'article', 'conference' or 'other'
* @instance
*/
getPublicationUniqueTypeFromPublication(publication) {
this.__isPublication(publication);
const aggregationType = String(publication['prism:aggregationType']).toLowerCase();
const subtypeDescription = String(publication['subtypeDescription']).toLowerCase();
if (aggregationType === 'journal') {
return 'article';
} else if (aggregationType === 'conference proceeding' ||
(aggregationType === 'book series' && subtypeDescription === 'conference paper')
) {
return 'conference';
} else {
return 'other';
}
}
/**
* Returns the ISSN of the journal where the article was published. This method is intended to be used with articles.
* For conference papers, it makes not much sense, since conferences ar retrieved for acronym, not ISSN
* @method getPublicationISSN
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of the publication
* @returns {String} The ISSN of the journal or book, or null if it's not present
* @instance
*/
getPublicationISSN(scopusId) {
let publication = this.getPublication(scopusId);
return publication['prism:issn'] || null;
}
/**
* Returns the ISSN of the journal where the article was published. This method is intended to be used with articles.
* For conference papers, it makes not much sense, since conferences ar retrieved for acronym, not ISSN
* @method getPublicationISSNFromPublication
* @memberof PublicationsManager
* @param {Object} publication The publication object
* @returns {String} The ISSN of the journal or book, or null if it's not present
* @instance
*/
getPublicationISSNFromPublication(publication) {
this.__isPublication(publication);
return publication['prism:issn'] || null;
}
/**
* Returns the journal name where the paper was published. This method is intended to be used with
* journal papers. For conferences, it makes not much sense.
* @method getPublicationJournalName
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of the publication
* @returns {String} The journal name of the publication
* @instance
*/
getPublicationJournalName(scopusId) {
const publication = this.getPublication(scopusId);
return this.getPublicationJournalNameFromPublication(publication);
}
/**
* Returns the journal name where the paper was published. This method is intended to be used with
* journal papers. For conferences, it makes not much sense.
* @method getPublicationJournalName
* @memberof PublicationsManager
* @param {Object} publication The publication object
* @returns {String} The journal name of the publication
* @instance
*/
getPublicationJournalNameFromPublication(publication) {
assert(this.getPublicationUniqueTypeFromPublication(publication) !== 'conference', 'The publication is not an article');
return publication['prism:publicationName'];
}
/**
* Returns the conference name where the paper was published. This method is intended to be used with
* conference papers. For articles, it makes not much sense.
* @method getPublicationConfName
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of the publication
* @returns {String} The conference name of the publication, or null if it's not present
* @instance
*/
getPublicationConfName(scopusId) {
let publication = this.getPublication(scopusId);
let confName;
try {
confName = publication.abstract.item.bibrecord.head.source["additional-srcinfo"].conferenceinfo.confevent.confname;
} catch (e) {
confName = null;
}
return confName;
}
/**
* Returns the conference name where the paper was published. This method is intended to be used with
* conference papers. For articles, it makes not much sense.
* @method getPublicationConfNameFromPublication
* @memberof PublicationsManager
* @param {Object} publication The publication object
* @returns {String} The conference name of the publication, or null if it's not present
* @instance
*/
getPublicationConfNameFromPublication(publication) {
this.__isPublication(publication);
let confName;
try {
confName = publication.abstract.item.bibrecord.head.source["additional-srcinfo"].conferenceinfo.confevent.confname;
} catch (e) {
confName = null;
}
return confName;
}
/**
* Returns an array with subject areas information in a publication of given scopus id:
* ```
* [
* {
* "@abbrev": "COMP",
* "@code": "1712",
* "$": "Software",
* "@_fa": "true"
* },
* {
* "@abbrev": "COMP",
* "@code": "1710",
* "$": "Information Systems",
* "@_fa": "true"
* },
* {
* "@abbrev": "COMP",
* "@code": "1708",
* "$": "Hardware and Architecture",
* "@_fa": "true"
* }
* ]
* ```
* @method getPublicationSubjectAreas
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {Array} Array with the information about subject areas of the publication
* @instance
*/
getPublicationSubjectAreas(scopusId) {
let publication = this.getPublication(scopusId);
return publication["abstract"]["subject-areas"]["subject-area"];
}
/**
* Returns an array with some information about authors of a publication of given scopus id:
* ```
* [
* {
* "@seq": "1",
* "@auid": "6507957743",
* "name": "Doe, Jane"
* },
* {
* "@seq": "2",
* "@auid": "22333454800",
* "name": "Doe J."
* },
* {
* "@seq": "3",
* "@auid": "24802465400",
* "name": "Doe Jane"
* },
* {
* "@seq": "4",
* "@auid": "15120180100",
* "name": "Doe, Jane"
* }
* ]
* ```
* "@seq" is the number that each author hold.
* "@auid" is the authorId of each author.
* @method getPublicationsAuthors
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @returns {Array} Array with authors information
* @instance
*/
getPublicationsAuthors(scopusId) {
let publication = this.getPublication(scopusId);
return publication["abstract"]["authors"]["author"].map(a => ({ '@seq': a['@seq'], '@auid': a['@auid'], 'name': a['ce:indexed-name'] }));
}
/**
* Returns an integer representing the number of cites of the publication with the given scopusId
* @method getPublicationCites
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {Number} Number of cites
* @instance
*/
getPublicationCites(scopusId) {
const publication = this.getPublication(scopusId);
return Number(publication['citedby-count']) || 0;
}
/**
* Returns a number with the publication cite score in the year it was published, or the last
* year with data
* @method getPublicationCiteScore
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {Number} Publication's cite score
* @instance
*/
getPublicationCiteScore(scopusId) {
const publication = this.getPublication(scopusId);
const publicationDate = this.__getPublicationDate(publication);
let res = null;
// If the publication has no date, the citeScore is null
if (publicationDate && _.has(publication, 'publisherStats.citeScoreYearInfoList.citeScoreYearInfo')) {
// We retrieve all the citeScore info about the publication and its year
const citeScoreList = publication.publisherStats.citeScoreYearInfoList.citeScoreYearInfo;
const publicationYear = publicationDate.getFullYear();
const totalYears = citeScoreList.length;
let yearsChecked = 0;
// Start looking for cite score info at the publication year. Stop when the curr year is 1900
// or when all the years available are checked
for (let currYear = publicationYear; currYear > 1900 && yearsChecked < totalYears; currYear--) {
// Get the info about that year
const citeScore = citeScoreList.find(element => element['@year'] === String(currYear));
// If there are info in that year and the status is not 'in progress'
if (citeScore && citeScore['@status'] !== 'In-Progress') {
// Retrieve the data and stop the loop
res = Number(citeScore['citeScoreInformationList'][0]['citeScoreInfo'][0]['citeScore']);
break;
}
yearsChecked++;
// Else, try with the previous year
}
}
return res;
}
/**
* Returns the date in which a publication was printed
* @method getPublicationPrintDate
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {Date?} Publication's print date of null if the publication hasn't been printed yet
* @instance
*/
getPublicationPrintDateFromPublication(publication) {
let res;
/* If the publication has both volume and cover date we can
assume that the publication has been printed*/
if (_.has(publication, 'prism:volume') &&
_.has(publication, 'prism:coverDate')) {
// If the date is in an unrecognized format, Date.parse will return
// NaN, so we have to return null
res = Date.parse(publication['prism:coverDate']) || null;
}
return res;
}
/**
* Returns the date in which a publication (by scopusId) was printed
* @method getPublicationPrintDate
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {Date?} Publication's print date of null if the publication hasn't been printed yet
* @instance
*/
getPublicationPrintDate(scopusId) {
const publication = this.getPublication(scopusId);
return this.getPublicationPrintDateFromPublication(publication);
}
/**
* Returns the volume in which the publication has been published
* @method getPublicationVolume
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {String?} Publication's volume or null if the publication hasn't been published yet
* @instance
*/
getPublicationVolume(scopusId) {
let res = null;
const publication = this.getPublication(scopusId);
if (_.has(publication, 'prism:volume')) {
res = String(publication['prism:volume']);
}
return res;
}
/**
* Returns the publication issue or, if it doesn't have issue, the pages.
* @method getPublicationIssueOrPages
* @memberof PublicationsManager
* @param {String} scopusId The scopus id of desired publication
* @return {String?} Publication's issue, pages or null if the publication has no issue
* @instance
*/
getPublicationIssueOrPages(scopusId) {
let res = null;
const publication = this.getPublication(scopusId);
if (_.has(publication, 'prism:issueIdentifier')) {
res = String(publication['prism:issueIdentifier']);
} else if (_.has(publication, 'prism:pageRange')) {
res = String(publication['prism:pageRange']);
}
return res;
}
/**
* Filters the publications to retain only the type passed as parameter.
* @method filterPublications
* @memberof PublicationsManager
* @param {String} publicationsType A string indicating the type of publications to retain.
* The accepted values are 'article', 'conference' or 'all'
* @return {PublicationsManager} Returns an object of the same type as this for chaining operations.
* The class publications attribute is the one that is modified.
* @throws {AssertionError} Throws an error `publicationsType`is null or undefined or if it's not a valid string
* @instance
*/
filterPublications(publicationsType) {
assert(!util.isNullOrUndefined(publicationsType), 'publicationsType can\'t be null or undefined');
assert(typeof (publicationsType) === 'string', 'publicationsType must be a string');
assert(publicationsType === 'conference' || publicationsType === 'article' || publicationsType === 'other'
|| publicationsType === 'all', 'publicationsType has to be article or conference');
this.publications = this.publications.filter((publication) => {
if (publicationsType === 'all') {
return true; //Accept everything if publicationsType is all
} else {
return this.getPublicationUniqueTypeFromPublication(publication) === publicationsType;
}
});
return this;
}
/**
* Returns the conference class of an enriched publication, by its scpusId. If the publication
* has been published in a journal or it's not enriched, null is returned.
* @param {Srting} scopusId The publication's scopusId you want to know its conference class
* @return {String?} The publication's conference class
*/
getPublicationConferenceClass(scopusId) {
const publication = this.getPublication(scopusId);
return this.getPublicationConferenceClassFromPublication(publication);
}
/**
* Returns the conference class of an enriched publication. If the publication has been
* published in a journal or it's not enriched, null is returned.
* @param {Object} publication The publication you want to know its conference class
* @return {String?} The publication's conference class
*/
getPublicationConferenceClassFromPublication(publication) {
let res = publication['conferenceClass'] || null;
if (res !== null) {
res = String(res);
}
return res;
}
/**
* Returns the conference rating of an enriched publication, by its scpusId. If the publication
* has been published in a journal or it's not enriched, null is returned.
* @param {Srting} scopusId The publication's scopusId you want to know its conference rating
* @return {String?} The publication's conference rating
*/
getPublicationConferenceRating(scopusId) {
const publication = this.getPublication(scopusId);
return this.getPublicationConferenceRatingFromPublication(publication);
}
/**
* Returns the conference rating of an enriched publication. If the publication has been
* published in a journal or it's not enriched, null is returned.
* @param {Object} publication The publication you want to know its conference rating
* @return {String?} The publication's conference rating
*/
getPublicationConferenceRatingFromPublication(publication) {
let res = publication['conferenceRating'] || null;
if (res !== null) {
res = String(res);
}
return res;
}
/**
* Returns the quality source of an enriched publication, by its scpusId.
* If the publication it's not enriched, null is returned.
* @param {Srting} scopusId The publication's scopusId you want to know its quality source
* @return {String?} The publication's quality source
*/
getPublicationQualitySource(scopusId) {
const publication = this.getPublication(scopusId);
return this.getPublicationQualitySourceFromPublication(publication);
}
/**
* Returns the quality source of an enriched publication. If the publication
* it's not enriched, null is returned.
* @param {Object} publication The publication you want to know its quality source
* @return {String?} The publication's quality source
*/
getPublicationQualitySourceFromPublication(publication) {
return publication['qualitySource'] || null;
}
/**
* Remove all duplicated publications in the array of publications.
* @method removeDuplicates
* @memberof PublicationsManager
* @return {PublicationsManager} Returns an object of the same type as this for chaining operations.
* The class publications attribute is the one that is modified.
* @instance
*/
removeDuplicates() {
this.publications = _.uniqBy(this.publications, "dc:identifier");
return this;
}
/**
* Returns a publication date in a Javascript Date object, or null if the publication has no date.
* @method __getPublicationDate
* @memberof PublicationsManager
* @param {Object} publication Publication object
* @returns {Date} Publication date
* @private
* @instance
*/
__getPublicationDate(publication) {
let publicationDate = null;
if (_.has(publication, 'abstract.item.bibrecord.head.source.publicationdate')) {
const publicationsDate = publication.abstract.item.bibrecord.head.source.publicationdate;
if (!publicationsDate.hasOwnProperty('day')) {
publicationsDate.day = 1;
}
if (!publicationsDate.hasOwnProperty('month')) {
publicationsDate.month = 1;
}
publicationDate = new Date(
parseInt(publicationsDate.year),
parseInt(publicationsDate.month) - 1, // In Javascript, January is month 0
parseInt(publicationsDate.day));
}
return publicationDate;
}
/**
* Checks if the parameter passed is a publication. If not, throws assertion error
* @method __isPublication
* @memberof PublicationsManager
* @param {Object} publication Publication object
* @returns {void} Nothing
* @throws {AssertionError} Throws an error if the parameter passed is not a publication object
* @instance
*/
__isPublication(publication) {
assert(!util.isNullOrUndefined(publication), "publication should not be null or undefined");
assert(typeof (publication) === 'object', "publication should be an object");
assert(_.has(publication, "dc:identifier"), "publication object must have a Scopus ID, aka 'dc:identifier' property");
}
/**
* Returns a publication object given a scopus id.
* @method getPublication
* @memberof PublicationsManager
* @param {String} scopusId Scopus id
* @returns {Object} publication Publication object
* @throws {AssertionError} Throws an error there is no given scopus id, it isn't a string or there is no publication with that id.
* @instance
*/
getPublication(scopusId) {
assert(typeof (scopusId) === 'string', "scopusId should be a string");
scopusId = 'SCOPUS_ID:' + scopusId;
let publication = this.publications.find(p => p['dc:identifier'] === scopusId);
assert(!util.isNullOrUndefined(publication), "Publication with scopus id " + scopusId + " not found");
return publication;
}
};