@@ -16,6 +16,19 @@ const AI_ENGINEERING_TRACK_NAMES = new Set([
1616 'AI_ENGINEER' ,
1717 'AI_ENGINEERING'
1818] )
19+ const NATIVE_DATA_SCIENCE_SUBTRACK_NAMES = [
20+ 'Challenge' ,
21+ 'MARATHON_MATCH'
22+ ]
23+ const NATIVE_DATA_SCIENCE_STATS_KEYS = new Set ( [
24+ ...NATIVE_DATA_SCIENCE_SUBTRACK_NAMES ,
25+ 'SRM' ,
26+ 'challenges' ,
27+ 'mostRecentEventDate' ,
28+ 'mostRecentEventName' ,
29+ 'mostRecentSubmission' ,
30+ 'wins'
31+ ] )
1932
2033/**
2134 * Return a finite numeric value without coercing strings or nullish values.
@@ -134,13 +147,9 @@ function getAIEngineeringSource (stats) {
134147 subTrack : { ...subTrack , name } ,
135148 trackName : 'DATA_SCIENCE'
136149 } ) )
137- . sort ( ( left , right ) => (
138- ( getFiniteNumber ( right . subTrack . rank && right . subTrack . rank . rating ) ?? 0 ) -
139- ( getFiniteNumber ( left . subTrack . rank && left . subTrack . rank . rating ) ?? 0 )
140- ) )
141150
142151 if ( dataScienceCandidates . length > 0 ) {
143- return dataScienceCandidates [ 0 ]
152+ return getDataScienceSummarySource ( dataScienceCandidates )
144153 }
145154
146155 const topLevelName = [ 'AI_ENGINEERING' , 'AI' , 'AI_ENGINEER' ]
@@ -264,11 +273,68 @@ function getDevelopmentTrackSummary (sources, statsHistory) {
264273 }
265274}
266275
276+ /**
277+ * Pick the Data Science subtrack whose rating Profiles displays.
278+ * Rating, percentile, and challenge count are descending tie breakers.
279+ * @param {Array<Object> } sources active rated Data Science sources
280+ * @returns {Object|undefined } source with the strongest visible rating
281+ */
282+ function getDataScienceSummarySource ( sources ) {
283+ return [ ...sources ] . sort ( ( left , right ) => {
284+ const leftRank = left . subTrack . rank || { }
285+ const rightRank = right . subTrack . rank || { }
286+
287+ return ( getFiniteNumber ( rightRank . rating ) ?? 0 ) - ( getFiniteNumber ( leftRank . rating ) ?? 0 ) ||
288+ ( getFiniteNumber ( rightRank . percentile ) ?? 0 ) - ( getFiniteNumber ( leftRank . percentile ) ?? 0 ) ||
289+ ( getFiniteNumber ( right . subTrack . challenges ) ?? 0 ) - ( getFiniteNumber ( left . subTrack . challenges ) ?? 0 )
290+ } ) [ 0 ]
291+ }
292+
293+ /**
294+ * Build the independently rated, non-native Data Science rows shown by Profiles.
295+ * Native Challenge, Marathon Match, and SRM rows are handled by their parent
296+ * tracks, while AI Engineering aliases remain grouped under Development.
297+ * @param {Object } stats member stats response for one public group
298+ * @param {Object|undefined } statsHistory member stats history response for the same group
299+ * @returns {Array<{trackName: string, rating: number, wins: number, submissions: number, challenges: number}> } custom rated rows
300+ */
301+ function getDataScienceRatingPathRows ( stats , statsHistory ) {
302+ const dataScienceStats : Record < string , any > = stats . DATA_SCIENCE
303+ if ( ! dataScienceStats || typeof dataScienceStats !== 'object' ) {
304+ return [ ]
305+ }
306+
307+ return Object . entries ( dataScienceStats )
308+ . filter ( ( [ name , subTrack ] ) => (
309+ ! NATIVE_DATA_SCIENCE_STATS_KEYS . has ( name ) &&
310+ ! isAIEngineeringTrackName ( name ) &&
311+ subTrack && typeof subTrack === 'object' &&
312+ getFiniteNumber ( subTrack . rank && subTrack . rank . rating ) !== undefined
313+ ) )
314+ . sort ( ( [ , left ] , [ , right ] ) => (
315+ ( getFiniteNumber ( right . wins ) ?? 0 ) - ( getFiniteNumber ( left . wins ) ?? 0 ) ||
316+ ( getSubTrackDisplaySubmissionCount ( right ) ?? 0 ) - ( getSubTrackDisplaySubmissionCount ( left ) ?? 0 )
317+ ) )
318+ . map ( ( [ name , subTrack ] ) => {
319+ const source = {
320+ subTrack : { ...subTrack , name } ,
321+ trackName : 'DATA_SCIENCE'
322+ }
323+
324+ return {
325+ trackName : name ,
326+ rating : getFiniteNumber ( subTrack . rank && subTrack . rank . rating ) ?? 0 ,
327+ ...getSubTrackSummary ( source , statsHistory ) ,
328+ challenges : getFiniteNumber ( subTrack . challenges ) ?? 0
329+ }
330+ } )
331+ }
332+
267333/**
268334 * Convert member stats and history responses into downloaded-profile activity rows.
269- * The PDF uses this mapper to match the Development, Design, Testing, and
270- * Competitive Programming values shown by Profiles. Competitive Programming is
271- * emitted only for active SRM stats; other Data Science activity is not relabeled .
335+ * The PDF uses this mapper to match the Development, Design, Testing, Data
336+ * Science, configured rating-path, and Competitive Programming values shown by
337+ * Profiles. Competitive Programming is emitted only for active SRM stats.
272338 * This function does not mutate its inputs or throw for missing response fields.
273339 * @param {Object|undefined } stats member stats response for one public group
274340 * @param {Object|undefined } statsHistory member stats history response for the same group
@@ -325,6 +391,26 @@ function buildProfileActivityStats (stats, statsHistory) {
325391 } )
326392 }
327393
394+ const dataScienceStats : Record < string , any > = stats . DATA_SCIENCE || { }
395+ const dataScienceSources = NATIVE_DATA_SCIENCE_SUBTRACK_NAMES
396+ . filter ( name => (
397+ dataScienceStats [ name ] &&
398+ typeof dataScienceStats [ name ] === 'object' &&
399+ ( getFiniteNumber ( dataScienceStats [ name ] . challenges ) ?? 0 ) > 0
400+ ) )
401+ . map ( name => ( {
402+ subTrack : { ...dataScienceStats [ name ] , name } ,
403+ trackName : 'DATA_SCIENCE'
404+ } ) )
405+ if ( dataScienceSources . length > 0 ) {
406+ const summarySource = getDataScienceSummarySource ( dataScienceSources )
407+ result . push ( {
408+ trackName : 'Data Science' ,
409+ rating : getFiniteNumber ( summarySource && summarySource . subTrack . rank && summarySource . subTrack . rank . rating ) ?? 0 ,
410+ ...getStandardTrackSummary ( dataScienceSources , statsHistory )
411+ } )
412+ }
413+
328414 const srmStats = stats . DATA_SCIENCE && stats . DATA_SCIENCE . SRM
329415 const competitions = getFiniteNumber ( srmStats && srmStats . challenges ) ?? 0
330416 if ( competitions > 0 ) {
@@ -336,9 +422,41 @@ function buildProfileActivityStats (stats, statsHistory) {
336422 } )
337423 }
338424
425+ result . push ( ...getDataScienceRatingPathRows ( stats , statsHistory ) )
426+
339427 return result
340428}
341429
430+ /**
431+ * Resolve stats requests and build downloaded-profile activity rows.
432+ * Member stats are required; history is optional and falls back to aggregate
433+ * counters when its request fails. Promise.all is compatible with the Bluebird
434+ * global used by the application bootstrap.
435+ * @param {Promise<Array<Object>> } statsRequest member stats request
436+ * @param {Promise<Array<Object>> } historyRequest member stats history request
437+ * @param {Function } [onHistoryFailure] optional history error callback
438+ * @returns {Promise<Array<{trackName: string, wins: number, submissions?: number, challenges?: number, rating?: number, competitions?: number}>> } PDF activity rows
439+ * @throws {* } when the required member stats request fails
440+ */
441+ async function buildProfileActivityStatsFromRequests ( statsRequest , historyRequest , onHistoryFailure ) {
442+ const safeHistoryRequest = Promise . resolve ( historyRequest ) . catch ( ( error ) => {
443+ if ( typeof onHistoryFailure === 'function' ) {
444+ onHistoryFailure ( error )
445+ }
446+ return [ ]
447+ } )
448+ const [ statsResult , historyResult ] = await Promise . all ( [
449+ statsRequest ,
450+ safeHistoryRequest
451+ ] )
452+
453+ return buildProfileActivityStats (
454+ Array . isArray ( statsResult ) ? statsResult [ 0 ] : undefined ,
455+ Array . isArray ( historyResult ) ? historyResult [ 0 ] : undefined
456+ )
457+ }
458+
342459module . exports = {
343- buildProfileActivityStats
460+ buildProfileActivityStats,
461+ buildProfileActivityStatsFromRequests
344462}
0 commit comments