diff --git a/README.md b/README.md index 535b74d..1a5322a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # make-exercise-javascript -Make Exercise in JavaScript + +This is a public repository. + +If you are from Make, check [this internal page](https://make.atlassian.net/wiki/x/oYBBmw) to see how to work with it. diff --git a/p1.js b/p1.js new file mode 100644 index 0000000..10730a8 --- /dev/null +++ b/p1.js @@ -0,0 +1,72 @@ +const ACTIVITIES = [ + { person: 'Alice', distance: 12.5, type: 'run', week: 'W01' }, + { person: 'Bob', distance: 8.0, type: 'run', week: 'W01' }, + { person: 'Carol', distance: 15.0, type: 'run', week: 'W01' }, + { person: 'Alice', distance: 2.0, type: 'swim', week: 'W01' }, + { person: 'David', distance: 3.5, type: 'swim', week: 'W01' }, + { person: 'Alice', distance: 18.0, type: 'run', week: 'W02' }, + { person: 'Bob', distance: 22.0, type: 'run', week: 'W02' }, + { person: 'Carol', distance: 10.0, type: 'run', week: 'W02' }, + { person: 'David', distance: 5.0, type: 'run', week: 'W02' }, + { person: 'Bob', distance: 1.5, type: 'swim', week: 'W02' }, + { person: 'Carol', distance: 4.0, type: 'swim', week: 'W02' }, + { person: 'Alice', distance: 25.0, type: 'run', week: 'W03' }, + { person: 'Bob', distance: 15.0, type: 'run', week: 'W03' }, + { person: 'Carol', distance: 20.0, type: 'run', week: 'W03' }, + { person: 'David', distance: 12.0, type: 'run', week: 'W03' }, + { person: 'Eve', distance: 8.0, type: 'run', week: 'W03' }, + { person: 'Alice', distance: 3.0, type: 'swim', week: 'W03' }, + { person: 'David', distance: 5.0, type: 'swim', week: 'W03' }, + { person: 'Eve', distance: 2.5, type: 'swim', week: 'W03' }, +]; + +function getTotalRunDistanceForPerson(personName) { + return 123; +} + +function getTopRunnerForGivenWeek(week) { + return { + person: 'Name', + distance: 123 + }; +} + +function get2TopPerformersByType() { + return { + run: [{ + person: 'Name', + distance: 123 + },{ + person: 'Name', + distance: 123 + }], + swim: [{ + person: 'Name', + distance: 123 + },{ + person: 'Name', + distance: 123 + }] + } +} + +function getMostImprovedRunner(startingWeek, endingWeek) { + return { + person: 'Name', + improvement: 123 + }; +} + +console.log('=== Task 1: getTotalRunDistanceForPerson ==='); +console.log('Alice:', getTotalRunDistanceForPerson('Alice')); +console.log('Bob:', getTotalRunDistanceForPerson('Bob')); + +console.log('\n=== Task 2: getTopRunnerForGivenWeek ==='); +console.log('W01:', JSON.stringify(getTopRunnerForGivenWeek('W01'))); +console.log('W02:', JSON.stringify(getTopRunnerForGivenWeek('W02'))); + +console.log('\n=== Task 3: get2TopPerformersByType ==='); +console.log(JSON.stringify(get2TopPerformersByType(), null, 2)); + +console.log('\n=== Task 4: getMostImprovedRunner ==='); +console.log('W01 to W03:', JSON.stringify(getMostImprovedRunner('W01', 'W03'))); diff --git a/p2/config/activity-types.json b/p2/config/activity-types.json new file mode 100644 index 0000000..59cebc6 --- /dev/null +++ b/p2/config/activity-types.json @@ -0,0 +1,17 @@ +{ + "run": { + "calorieRate": 10.5, + "category": "cardio", + "displayName": "Running" + }, + "swim": { + "calorieRate": 14.2, + "category": "cardio", + "displayName": "Swimming" + }, + "bike": { + "calorieRate": 8.3, + "category": "cardio", + "displayName": "Cycling" + } +} diff --git a/p2/data/activities.json b/p2/data/activities.json new file mode 100644 index 0000000..ca4568b --- /dev/null +++ b/p2/data/activities.json @@ -0,0 +1,46 @@ +[ + { "person": "Alice", "distance": 12.5, "type": "run", "week": "W01", "date": "2024-01-03", "duration": 75, "heartRate": 165, "elevationGain": 120 }, + { "person": "Bob", "distance": 8.0, "type": "run", "week": "W01", "date": "2024-01-04", "duration": 55, "heartRate": 152, "elevationGain": 50 }, + { "person": "Carol", "distance": 15.0, "type": "run", "week": "W01", "date": "2024-01-05", "duration": 90, "heartRate": 158, "elevationGain": 200 }, + { "person": "Alice", "distance": 2.0, "type": "swim", "week": "W01", "date": "2024-01-06", "duration": 45, "heartRate": 135, "elevationGain": 0 }, + { "person": "David", "distance": 3.5, "type": "swim", "week": "W01", "date": "2024-01-07", "duration": 60, "heartRate": 128, "elevationGain": 0 }, + + { "person": "Alice", "distance": 18.0, "type": "run", "week": "W02", "date": "2024-01-10", "duration": 95, "heartRate": 168, "elevationGain": 180 }, + { "person": "Bob", "distance": 22.0, "type": "run", "week": "W02", "date": "2024-01-11", "duration": 125, "heartRate": 162, "elevationGain": 250 }, + { "person": "Carol", "distance": 10.0, "type": "run", "week": "W02", "date": "2024-01-12", "duration": 60, "heartRate": 155, "elevationGain": 80 }, + { "person": "David", "distance": 5.0, "type": "run", "week": "W02", "date": "2024-01-13", "duration": 38, "heartRate": 148, "elevationGain": 30 }, + { "person": "Bob", "distance": 1.5, "type": "swim", "week": "W02", "date": "2024-01-14", "duration": 35, "heartRate": 132, "elevationGain": 0 }, + { "person": "Carol", "distance": 4.0, "type": "swim", "week": "W02", "date": "2024-01-14", "duration": 70, "heartRate": 142, "elevationGain": 0 }, + + { "person": "Alice", "distance": 25.0, "type": "run", "week": "W03", "date": "2024-01-17", "duration": 135, "heartRate": 172, "elevationGain": 320 }, + { "person": "Bob", "distance": 15.0, "type": "run", "week": "W03", "date": "2024-01-18", "duration": 88, "heartRate": 158, "elevationGain": 150 }, + { "person": "Carol", "distance": 20.0, "type": "run", "week": "W03", "date": "2024-01-19", "duration": 115, "heartRate": 165, "elevationGain": 280 }, + { "person": "David", "distance": 12.0, "type": "run", "week": "W03", "date": "2024-01-20", "duration": 78, "heartRate": 153, "elevationGain": 110 }, + { "person": "Eve", "distance": 8.0, "type": "run", "week": "W03", "date": "2024-01-21", "duration": 52, "heartRate": 145, "elevationGain": 60 }, + { "person": "Alice", "distance": 3.0, "type": "swim", "week": "W03", "date": "2024-01-21", "duration": 52, "heartRate": 138, "elevationGain": 0 }, + { "person": "David", "distance": 5.0, "type": "swim", "week": "W03", "date": "2024-01-21", "duration": 82, "heartRate": 130, "elevationGain": 0 }, + { "person": "Eve", "distance": 2.5, "type": "swim", "week": "W03", "date": "2024-01-21", "duration": 48, "heartRate": 125, "elevationGain": 0 }, + + { "person": "Alice", "distance": 30.0, "type": "run", "week": "W04", "date": "2024-01-24", "duration": 155, "heartRate": 175, "elevationGain": 410 }, + { "person": "Bob", "distance": 28.0, "type": "run", "week": "W04", "date": "2024-01-25", "duration": 148, "heartRate": 168, "elevationGain": 380 }, + { "person": "Carol", "distance": 18.0, "type": "run", "week": "W04", "date": "2024-01-26", "duration": 102, "heartRate": 160, "elevationGain": 200 }, + { "person": "David", "distance": 10.0, "type": "run", "week": "W04", "date": "2024-01-27", "duration": 65, "heartRate": 150, "elevationGain": 90 }, + { "person": "Bob", "distance": 2.0, "type": "swim", "week": "W04", "date": "2024-01-28", "duration": 42, "heartRate": 135, "elevationGain": 0 }, + { "person": "Carol", "distance": 5.5, "type": "swim", "week": "W04", "date": "2024-01-28", "duration": 88, "heartRate": 145, "elevationGain": 0 }, + { "person": "Eve", "distance": 3.0, "type": "swim", "week": "W04", "date": "2024-01-28", "duration": 55, "heartRate": 128, "elevationGain": 0 }, + + { "person": "Alice", "distance": 35.0, "type": "run", "week": "W05", "date": "2024-01-31", "duration": 175, "heartRate": 178, "elevationGain": 520 }, + { "person": "Bob", "distance": 20.0, "type": "run", "week": "W05", "date": "2024-02-01", "duration": 112, "heartRate": 165, "elevationGain": 220 }, + { "person": "Carol", "distance": 25.0, "type": "run", "week": "W05", "date": "2024-02-02", "duration": 142, "heartRate": 170, "elevationGain": 350 }, + { "person": "David", "distance": 15.0, "type": "run", "week": "W05", "date": "2024-02-03", "duration": 95, "heartRate": 158, "elevationGain": 180 }, + { "person": "Eve", "distance": 12.0, "type": "run", "week": "W05", "date": "2024-02-04", "duration": 78, "heartRate": 152, "elevationGain": 140 }, + { "person": "Alice", "distance": 4.0, "type": "swim", "week": "W05", "date": "2024-02-04", "duration": 65, "heartRate": 140, "elevationGain": 0 }, + { "person": "David", "distance": 6.0, "type": "swim", "week": "W05", "date": "2024-02-04", "duration": 95, "heartRate": 135, "elevationGain": 0 }, + { "person": "Eve", "distance": 4.5, "type": "swim", "week": "W05", "date": "2024-02-04", "duration": 72, "heartRate": 130, "elevationGain": 0 }, + + { "person": "Bob", "distance": 45.0, "type": "bike", "week": "W01", "date": "2024-01-05", "duration": 105, "heartRate": 142, "elevationGain": 180 }, + { "person": "Carol", "distance": 60.0, "type": "bike", "week": "W02", "date": "2024-01-12", "duration": 135, "heartRate": 148, "elevationGain": 240 }, + { "person": "Alice", "distance": 55.0, "type": "bike", "week": "W03", "date": "2024-01-19", "duration": 125, "heartRate": 155, "elevationGain": 220 }, + { "person": "Eve", "distance": 50.0, "type": "bike", "week": "W04", "date": "2024-01-26", "duration": 115, "heartRate": 145, "elevationGain": 200 }, + { "person": "David", "distance": 70.0, "type": "bike", "week": "W05", "date": "2024-02-02", "duration": 155, "heartRate": 152, "elevationGain": 280 } +] diff --git a/p2/data/loader.js b/p2/data/loader.js new file mode 100644 index 0000000..6bdd724 --- /dev/null +++ b/p2/data/loader.js @@ -0,0 +1,147 @@ +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export function loadActivities() { + const filePath = join(__dirname, 'activities.json'); + + try { + const rawData = readFileSync(filePath, 'utf-8'); + const activities = JSON.parse(rawData); + + if (!Array.isArray(activities)) { + throw new Error('Activities data must be an array'); + } + + const validatedActivities = activities.map((activity, index) => { + validateActivity(activity, index); + return activity; + }); + + return validatedActivities; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Activities file not found: ${filePath}`); + } + throw error; + } +} + +export function loadActivityTypes() { + const filePath = join(__dirname, '../config/activity-types.json'); + + try { + const rawData = readFileSync(filePath, 'utf-8'); + const activityTypes = JSON.parse(rawData); + + if (typeof activityTypes !== 'object' || activityTypes === null) { + throw new Error('Activity types must be an object'); + } + + for (const [type, config] of Object.entries(activityTypes)) { + validateActivityTypeConfig(type, config); + } + + return activityTypes; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Activity types config not found: ${filePath}`); + } + throw error; + } +} + +function validateActivity(activity, index) { + const requiredFields = ['person', 'distance', 'type', 'week', 'date', 'duration', 'heartRate', 'elevationGain']; + + for (const field of requiredFields) { + if (!(field in activity)) { + throw new Error(`Activity at index ${index} missing required field: ${field}`); + } + } + + if (typeof activity.person !== 'string' || activity.person.trim() === '') { + throw new Error(`Activity at index ${index} has invalid person name`); + } + + if (typeof activity.distance !== 'number' || activity.distance < 0) { + throw new Error(`Activity at index ${index} has invalid distance: ${activity.distance}`); + } + + if (typeof activity.type !== 'string' || activity.type.trim() === '') { + throw new Error(`Activity at index ${index} has invalid type`); + } + + if (typeof activity.week !== 'string' || !activity.week.match(/^W\d+$/)) { + throw new Error(`Activity at index ${index} has invalid week format: ${activity.week}`); + } + + if (typeof activity.date !== 'string' || !activity.date.match(/^\d{4}-\d{2}-\d{2}$/)) { + throw new Error(`Activity at index ${index} has invalid date format: ${activity.date}`); + } + + if (typeof activity.duration !== 'number' || activity.duration <= 0) { + throw new Error(`Activity at index ${index} has invalid duration: ${activity.duration}`); + } + + if (typeof activity.heartRate !== 'number' || activity.heartRate < 40 || activity.heartRate > 220) { + throw new Error(`Activity at index ${index} has invalid heart rate: ${activity.heartRate}`); + } + + if (typeof activity.elevationGain !== 'number' || activity.elevationGain < 0) { + throw new Error(`Activity at index ${index} has invalid elevation gain: ${activity.elevationGain}`); + } +} + +function validateActivityTypeConfig(type, config) { + if (typeof config !== 'object' || config === null) { + throw new Error(`Activity type '${type}' config must be an object`); + } + + const requiredFields = ['calorieRate', 'category', 'displayName']; + for (const field of requiredFields) { + if (!(field in config)) { + throw new Error(`Activity type '${type}' missing required field: ${field}`); + } + } + + if (typeof config.calorieRate !== 'number' || config.calorieRate <= 0) { + throw new Error(`Activity type '${type}' has invalid calorieRate: ${config.calorieRate}`); + } + + if (typeof config.category !== 'string' || config.category.trim() === '') { + throw new Error(`Activity type '${type}' has invalid category`); + } + + if (typeof config.displayName !== 'string' || config.displayName.trim() === '') { + throw new Error(`Activity type '${type}' has invalid displayName`); + } +} + +export function validateActivityReferences(activities, activityTypes) { + const validTypes = new Set(Object.keys(activityTypes)); + const invalidActivities = []; + + for (let i = 0; i < activities.length; i++) { + if (!validTypes.has(activities[i].type)) { + invalidActivities.push({ + index: i, + type: activities[i].type, + person: activities[i].person + }); + } + } + + if (invalidActivities.length > 0) { + const details = invalidActivities + .map(a => ` - Index ${a.index}: type '${a.type}' (${a.person})`) + .join('\n'); + throw new Error( + `Found ${invalidActivities.length} activities with undefined activity types:\n${details}\n` + + `Valid types: ${Array.from(validTypes).join(', ')}` + ); + } +} diff --git a/p2/index.js b/p2/index.js new file mode 100644 index 0000000..e240e20 --- /dev/null +++ b/p2/index.js @@ -0,0 +1,34 @@ +import { loadActivities, loadActivityTypes, validateActivityReferences } from './data/loader.js'; +import { generateStatistics } from './services/stats-calculator.js'; +import { formatStatisticsReport, formatConsoleLog } from './reports/formatter.js'; + +function main() { + try { + console.log('Loading activity data...\n'); + + const activities = loadActivities(); + const activityTypes = loadActivityTypes(); + + validateActivityReferences(activities, activityTypes); + + console.log(`Loaded ${activities.length} activities`); + console.log(`Activity types: ${Object.keys(activityTypes).join(', ')}\n`); + + const stats = generateStatistics(activities, activityTypes); + + console.log(formatConsoleLog(stats.teamTotals)); + + const report = formatStatisticsReport(stats, activityTypes); + console.log('\n' + report); + + console.log('\n=== Full Statistics JSON ===\n'); + console.log(JSON.stringify(stats, null, 2)); + + } catch (error) { + console.error('Error generating statistics:'); + console.error(error.message); + process.exit(1); + } +} + +main(); diff --git a/p2/reports/formatter.js b/p2/reports/formatter.js new file mode 100644 index 0000000..f0f99bc --- /dev/null +++ b/p2/reports/formatter.js @@ -0,0 +1,113 @@ +export function formatTeamTotals(teamTotals, activityTypes) { + const lines = []; + + lines.push('=== Team Totals ===\n'); + + for (const [type, data] of Object.entries(teamTotals)) { + const displayName = activityTypes[type]?.displayName || type; + lines.push(`${displayName}: ${data.count} activities, ${data.totalDistance}km total`); + } + + return lines.join('\n'); +} + +export function formatPersonalStats(title, personalStats, activityTypes) { + const lines = []; + + lines.push(`\n=== ${title} ===\n`); + + for (const [type, rankings] of Object.entries(personalStats)) { + const displayName = activityTypes[type]?.displayName || type; + lines.push(`${displayName}:`); + + if (rankings.length === 0) { + lines.push(' (no data)'); + } else { + for (const ranking of rankings) { + const valueKey = Object.keys(ranking).find(k => k !== 'person'); + const value = ranking[valueKey]; + lines.push(` ${ranking.person}: ${value}`); + } + } + + lines.push(''); + } + + return lines.join('\n'); +} + +export function formatStatisticsReport(stats, activityTypes) { + const sections = []; + + sections.push(formatTeamTotals(stats.teamTotals, activityTypes)); + sections.push(formatPersonalStats('Average Distance per Person', stats.personal.averagePerPerson, activityTypes)); + sections.push(formatPersonalStats('Personal Bests', stats.personal.personalBests, activityTypes)); + sections.push(formatPersonalStats('Top 3 Weekly Appearances', stats.personal.top3Appearances, activityTypes)); + + return sections.join('\n'); +} + +export function formatRanking(title, rankings, valueLabel) { + const lines = []; + + lines.push(`\n=== ${title} ===\n`); + + if (rankings.length === 0) { + lines.push('(no data)'); + } else { + for (let i = 0; i < rankings.length; i++) { + const ranking = rankings[i]; + const valueKey = Object.keys(ranking).find(k => k !== 'person'); + const value = ranking[valueKey]; + lines.push(`${i + 1}. ${ranking.person}: ${value} ${valueLabel}`); + } + } + + return lines.join('\n'); +} + +export function formatWeeklyParticipation(weeklyData) { + const lines = []; + + lines.push('\n=== Weekly Participation ===\n'); + + for (const data of weeklyData) { + lines.push(`${data.week}: ${data.participantCount} participants`); + } + + return lines.join('\n'); +} + +export function formatAsJSON(stats) { + return JSON.stringify(stats, null, 2); +} + +export function formatActivityTypeSummary(byTypeData, activityTypes) { + const lines = []; + + lines.push('\n=== Activity Summary by Type ===\n'); + lines.push('Type | Activities | Distance | Participants'); + lines.push('--------------|------------|----------|-------------'); + + for (const [type, data] of Object.entries(byTypeData)) { + const displayName = (activityTypes[type]?.displayName || type).padEnd(13); + const count = String(data.totalActivities).padEnd(10); + const distance = `${Math.round(data.totalDistance)}km`.padEnd(8); + const participants = String(data.participants).padEnd(12); + + lines.push(`${displayName} | ${count} | ${distance} | ${participants}`); + } + + return lines.join('\n'); +} + +export function formatConsoleLog(teamTotals) { + const parts = []; + + for (const [type, data] of Object.entries(teamTotals)) { + const typeName = type.charAt(0).toUpperCase() + type.slice(1) + 's'; + parts.push(`${typeName}: ${data.count}, ${data.totalDistance}km`); + } + + return parts.join('. '); +} diff --git a/p2/services/aggregator.js b/p2/services/aggregator.js new file mode 100644 index 0000000..74ef979 --- /dev/null +++ b/p2/services/aggregator.js @@ -0,0 +1,154 @@ +export function groupBy(activities, field) { + const groups = {}; + + for (const activity of activities) { + const key = activity[field]; + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(activity); + } + + return groups; +} + +export function groupByMultiple(activities, fields) { + if (fields.length === 0) { + return activities; + } + + const [firstField, ...remainingFields] = fields; + const groups = groupBy(activities, firstField); + + if (remainingFields.length === 0) { + return groups; + } + + const nestedGroups = {}; + for (const [key, groupActivities] of Object.entries(groups)) { + nestedGroups[key] = groupByMultiple(groupActivities, remainingFields); + } + + return nestedGroups; +} + +export function filterBy(activities, field, value) { + return activities.filter(activity => activity[field] === value); +} + +export function filterByType(activities, type) { + return filterBy(activities, 'type', type); +} + +export function filterByPerson(activities, person) { + return filterBy(activities, 'person', person); +} + +export function filterByWeek(activities, week) { + return filterBy(activities, 'week', week); +} + +export function getUniqueValues(activities, field) { + const values = new Set(activities.map(activity => activity[field])); + return Array.from(values).sort(); +} + +export function aggregate(activities, field, operation = 'sum') { + if (activities.length === 0) { + return 0; + } + + const values = activities.map(activity => activity[field]); + + switch (operation) { + case 'sum': + return values.reduce((sum, val) => sum + val, 0); + case 'avg': + return values.reduce((sum, val) => sum + val, 0) / values.length; + case 'min': + return Math.min(...values); + case 'max': + return Math.max(...values); + case 'count': + return values.length; + default: + throw new Error(`Unknown aggregation operation: ${operation}`); + } +} + +export function summarizeGroups(groupedActivities, field) { + const summaries = {}; + + for (const [key, activities] of Object.entries(groupedActivities)) { + summaries[key] = { + count: activities.length, + total: aggregate(activities, field, 'sum'), + average: aggregate(activities, field, 'avg'), + min: aggregate(activities, field, 'min'), + max: aggregate(activities, field, 'max') + }; + } + + return summaries; +} + +export function aggregateByPerson(activities) { + const byPerson = groupBy(activities, 'person'); + const results = {}; + + for (const [person, personActivities] of Object.entries(byPerson)) { + results[person] = { + totalActivities: personActivities.length, + totalDistance: aggregate(personActivities, 'distance', 'sum'), + totalDuration: aggregate(personActivities, 'duration', 'sum'), + avgDistance: aggregate(personActivities, 'distance', 'avg'), + avgDuration: aggregate(personActivities, 'duration', 'avg'), + avgHeartRate: aggregate(personActivities, 'heartRate', 'avg'), + totalElevation: aggregate(personActivities, 'elevation', 'sum'), + activities: personActivities + }; + } + + return results; +} + +export function aggregateByWeek(activities) { + const byWeek = groupBy(activities, 'week'); + const results = {}; + + for (const [week, weekActivities] of Object.entries(byWeek)) { + const uniqueParticipants = getUniqueValues(weekActivities, 'person'); + + results[week] = { + totalActivities: weekActivities.length, + participants: uniqueParticipants.length, + totalDistance: aggregate(weekActivities, 'distance', 'sum'), + totalDuration: aggregate(weekActivities, 'duration', 'sum'), + avgDistance: aggregate(weekActivities, 'distance', 'avg'), + activities: weekActivities + }; + } + + return results; +} + +export function aggregateByType(activities) { + const byType = groupBy(activities, 'type'); + const results = {}; + + for (const [type, typeActivities] of Object.entries(byType)) { + const uniqueParticipants = getUniqueValues(typeActivities, 'person'); + + results[type] = { + totalActivities: typeActivities.length, + participants: uniqueParticipants.length, + totalDistance: aggregate(typeActivities, 'distance', 'sum'), + totalDuration: aggregate(typeActivities, 'duration', 'sum'), + avgDistance: aggregate(typeActivities, 'distance', 'avg'), + avgHeartRate: aggregate(typeActivities, 'heartRate', 'avg'), + activities: typeActivities + }; + } + + return results; +} diff --git a/p2/services/stats-calculator.js b/p2/services/stats-calculator.js new file mode 100644 index 0000000..7b492e9 --- /dev/null +++ b/p2/services/stats-calculator.js @@ -0,0 +1,168 @@ +import { groupBy, aggregate, filterByType, getUniqueValues } from './aggregator.js'; + +export function calculateAveragePerPerson(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const total = aggregate(personActivities, 'distance', 'sum'); + const count = personActivities.length; + const average = Math.round((total / count) * 10) / 10; + + results.push({ person, average }); + } + + return results.sort((a, b) => b.average - a.average); +} + +export function calculatePersonalBests(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const best = aggregate(personActivities, 'distance', 'max'); + results.push({ person, best }); + } + + return results.sort((a, b) => b.best - a.best); +} + +export function calculateTop3Appearances(activities) { + const byWeek = groupBy(activities, 'week'); + const appearances = {}; + + for (const weekActivities of Object.values(byWeek)) { + const personTotals = {}; + + for (const activity of weekActivities) { + if (!personTotals[activity.person]) { + personTotals[activity.person] = 0; + } + personTotals[activity.person] += activity.distance; + } + + const top3 = Object.entries(personTotals) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([person]) => person); + + for (const person of top3) { + appearances[person] = (appearances[person] || 0) + 1; + } + } + + return Object.entries(appearances) + .map(([person, count]) => ({ person, count })) + .sort((a, b) => b.count - a.count); +} + +export function calculateTeamTotalsByType(activities) { + const types = getUniqueValues(activities, 'type'); + const results = {}; + + for (const type of types) { + const typeActivities = filterByType(activities, type); + results[type] = { + count: typeActivities.length, + totalDistance: Math.round(aggregate(typeActivities, 'distance', 'sum')) + }; + } + + return results; +} + +export function generateStatistics(activities, activityTypes) { + const types = getUniqueValues(activities, 'type'); + + const teamTotals = calculateTeamTotalsByType(activities); + + const personal = { + averagePerPerson: {}, + personalBests: {}, + top3Appearances: {} + }; + + for (const type of types) { + const typeActivities = filterByType(activities, type); + personal.averagePerPerson[type] = calculateAveragePerPerson(typeActivities); + personal.personalBests[type] = calculatePersonalBests(typeActivities); + personal.top3Appearances[type] = calculateTop3Appearances(typeActivities); + } + + return { + teamTotals, + personal + }; +} + +export function calculateTotalDistancePerPerson(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const totalDistance = aggregate(personActivities, 'distance', 'sum'); + results.push({ person, totalDistance: Math.round(totalDistance * 10) / 10 }); + } + + return results.sort((a, b) => b.totalDistance - a.totalDistance); +} + +export function calculateAveragePace(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const totalDuration = aggregate(personActivities, 'duration', 'sum'); + const totalDistance = aggregate(personActivities, 'distance', 'sum'); + const avgPace = totalDistance > 0 ? totalDuration / totalDistance : 0; + + results.push({ + person, + avgPace: Math.round(avgPace * 10) / 10 + }); + } + + return results.sort((a, b) => a.avgPace - b.avgPace); +} + +export function calculateWeeklyParticipation(activities) { + const byWeek = groupBy(activities, 'week'); + const results = []; + + for (const [week, weekActivities] of Object.entries(byWeek)) { + const uniqueParticipants = getUniqueValues(weekActivities, 'person'); + results.push({ + week, + participantCount: uniqueParticipants.length + }); + } + + return results.sort((a, b) => a.week.localeCompare(b.week)); +} + +export function calculateTotalElevation(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const totalElevation = aggregate(personActivities, 'elevationGain', 'sum'); + results.push({ person, totalElevation: Math.round(totalElevation) }); + } + + return results.sort((a, b) => b.totalElevation - a.totalElevation); +} + +export function calculateAverageHeartRate(activities) { + const byPerson = groupBy(activities, 'person'); + const results = []; + + for (const [person, personActivities] of Object.entries(byPerson)) { + const avgHeartRate = aggregate(personActivities, 'heartRate', 'avg'); + results.push({ + person, + avgHeartRate: Math.round(avgHeartRate) + }); + } + + return results.sort((a, b) => b.avgHeartRate - a.avgHeartRate); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..89e6ba5 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "activities-stats", + "version": "1.0.0", + "scripts": { + "p1": "node p1.js", + "p2": "node p2/index.js" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC" +} +