Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f568dbe
Challenge #1 - Expose endpoint to return all users with >= 2 bad days…
tylerwill3000 Oct 1, 2025
0f1605b
Challenge #2 - Expose endpoint to return 5 longest streaks of bad days
tylerwill3000 Oct 1, 2025
0f6cb18
Add tests and fix exposed bugs
tylerwill3000 Oct 1, 2025
10e09bb
Add tests and fix exposed bugs
tylerwill3000 Oct 1, 2025
6d8aac9
Refactor to LocalDate
tylerwill3000 Oct 1, 2025
7dba173
Use more efficient test context
tylerwill3000 Oct 1, 2025
6cf7f46
Query username up-front when fetching BadDayDto to avoid an additiona…
tylerwill3000 Oct 1, 2025
69dc513
Compare BadDayDto via LocalDate by default
tylerwill3000 Oct 2, 2025
d29661c
Remove utils class
tylerwill3000 Oct 2, 2025
ad928f4
Use port 8090 since I already have a server running on 8080 in my loc…
tylerwill3000 Oct 2, 2025
ae0d64a
Use coalesce to more accurately express comparison against mood when …
tylerwill3000 Oct 2, 2025
48dbef3
Refactor code to parse bad day streaks
tylerwill3000 Oct 2, 2025
23917ae
Improve performance of bad day streak endpoint
tylerwill3000 Oct 2, 2025
c8d13c1
Update java / gradle / spring boot
tylerwill3000 Oct 16, 2025
18605c8
Use groupingBy() collector to compute bad days map
tylerwill3000 Oct 16, 2025
26e4bda
Remove @Transactional annotation
tylerwill3000 Oct 16, 2025
d38c0a7
Experiment with using the new stream gatherer API to process consecut…
tylerwill3000 Oct 17, 2025
1b3703e
Add explicit comparator for bad day ordering
tylerwill3000 Oct 17, 2025
3d8eecc
Simplify record class name
tylerwill3000 Oct 17, 2025
1d92d32
Improve variable names and print debug formatting
tylerwill3000 Oct 17, 2025
317d87c
Improve variable names
tylerwill3000 Oct 17, 2025
25ddbf0
Use records for immutable spring classes to remove lombok dependency
tylerwill3000 Oct 17, 2025
8ae4c5c
Comment out debugging line
tylerwill3000 Oct 17, 2025
29e56dd
Streamline getStartOfWeek function
tylerwill3000 Oct 17, 2025
a1143c7
Resolve weak warning
tylerwill3000 Oct 17, 2025
c19d2c1
Revert service to a class so we don't expose the repository members i…
tylerwill3000 Oct 17, 2025
41db0af
Formatting & small syntax adjustments
tylerwill3000 Oct 19, 2025
ce95498
Adding local performance tracer library for testing
tylerwill3000 Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions OddJava/build.gradle
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
plugins {
id 'org.springframework.boot' version '3.3.2'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
id 'groovy'
}

group = 'com.oddball'
version = '0.0.2-SNAPSHOT'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenLocal()
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}

dependencies {
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2:2.3.230'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-csv:1.11.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3'
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-csv:1.11.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'

runtimeOnly 'com.h2database:h2:2.3.230'
runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'com.github.tylerwilliams:performance-tracer:0.0.1'

testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.spockframework:spock-core:2.4-M6-groovy-4.0'
testImplementation 'org.spockframework:spock-spring:2.4-M6-groovy-4.0'
}

test {
Expand Down
2 changes: 1 addition & 1 deletion OddJava/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
5 changes: 5 additions & 0 deletions OddJava/src/main/java/com/oddball/challenges/BadDay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.oddball.challenges;

import java.time.LocalDate;

public record BadDay(long userId, String userName, LocalDate date) {}
27 changes: 27 additions & 0 deletions OddJava/src/main/java/com/oddball/challenges/BadDayController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.oddball.challenges;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/bad-day")
@RequiredArgsConstructor
public class BadDayController {
private final BadDayService badDayService;

@GetMapping("/weeks/{weekToCheck}")
public List<BadWeek> getBadWeeks(@PathVariable LocalDate weekToCheck) {
return badDayService.getBadWeeks(weekToCheck);
}

@GetMapping("/streaks")
public List<BadDayStreak> getBadDayStreaks() {
return badDayService.getBadDayStreaks();
}
}
93 changes: 93 additions & 0 deletions OddJava/src/main/java/com/oddball/challenges/BadDayService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.oddball.challenges;

import com.oddball.challenges.mood.MoodRepository;
import com.oddball.challenges.stress.StressRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toCollection;

@Service
@RequiredArgsConstructor
public class BadDayService {
private static final int MIN_BAD_DAYS_THRESHOLD = 2;
private static final int MAX_BAD_DAY_STREAKS = 5;

private final MoodRepository moodRepository;
private final StressRepository stressRepository;

public List<BadWeek> getBadWeeks(LocalDate weekToCheck) {
LocalDate startOfWeek = getStartOfWeek(weekToCheck);
LocalDate endOfWeek = startOfWeek.plusDays(6);

Map<Long, TreeSet<BadDay>> badDaysByUser = getBadDaysByUser(startOfWeek, endOfWeek);

return badDaysByUser.values()
.stream()
.map(userBadDays -> {
// each batch of bad days belong to a single user, so we can just grab the first one to get the username
String userName = userBadDays.getFirst().userName();
return new BadWeek(userName, userBadDays.size());
})
.filter(bw -> bw.numberOfBadDays() >= MIN_BAD_DAYS_THRESHOLD)
.sorted(comparing(BadWeek::numberOfBadDays).reversed())
.toList();
}

public List<BadDayStreak> getBadDayStreaks() {
Map<Long, TreeSet<BadDay>> allBadDays = getBadDaysByUser(null, null);

List<List<BadDay>> allBadDayStreaks = new ArrayList<>();
for (TreeSet<BadDay> userBadDays : allBadDays.values()) {
List<List<BadDay>> userStreaks = userBadDays.stream()
.gather(BadDayStreakGatherer.INSTANCE)
// .peek(streak ->
// System.out.println("Found streak:\n" + streak.stream().map(o -> " " + o).collect(joining("\n"))))
.toList();
allBadDayStreaks.addAll(userStreaks);
}

return allBadDayStreaks.stream()
.sorted((a, b) -> Integer.compare(b.size(), a.size())) // largest streaks to smallest
.limit(MAX_BAD_DAY_STREAKS)
.map(streak -> {
BadDay firstBadDay = streak.getFirst();
return new BadDayStreak(firstBadDay.userName(), firstBadDay.date(), streak.size());
})
.toList();
}

/**
* Get all bad days (from mood and stress) grouped by user for a given date range inclusive
* @param from Starting day (inclusive) to retrieve bad days for. Null = no lower bound
* @param to Ending day (inclusive) to retrieve bad days for. Null = no upper bound
* @return A mapping of user IDs to a sorted set of bad days (sorted by day) which fall within the specified date range
*/
private Map<Long, TreeSet<BadDay>> getBadDaysByUser(LocalDate from, LocalDate to) {
List<BadDay> badMoodDays = moodRepository.getBadMoodDays(from, to);
List<BadDay> badStressDays = stressRepository.getBadStressDays(from, to);

Stream<BadDay> allBadDays = Stream.concat(badMoodDays.stream(), badStressDays.stream());
return allBadDays.collect(
groupingBy(BadDay::userId, toCollection(() -> new TreeSet<>(comparing(BadDay::date)))));
}

/**
* @return The Sunday of the week that the given date falls within
*/
private static LocalDate getStartOfWeek(LocalDate date) {
return date.getDayOfWeek() == DayOfWeek.SUNDAY
? date
: date.minusDays(date.getDayOfWeek().getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.oddball.challenges;

import java.time.LocalDate;

public record BadDayStreak(String userName, LocalDate startDate, int streakLength) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.oddball.challenges;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Gatherer;

/**
* A {@link Gatherer} that collects consecutive {@link BadDay}s into streaks and emits each one as a list.
* This gatherer assumes that the input stream of {@link BadDay}s are sorted in chronological order.
*/
class BadDayStreakGatherer implements Gatherer<BadDay, List<BadDay>, List<BadDay>> {
static final BadDayStreakGatherer INSTANCE = new BadDayStreakGatherer();

private BadDayStreakGatherer() {}

@Override
public Supplier<List<BadDay>> initializer() {
return ArrayList::new;
}

@Override
public Gatherer.Integrator<List<BadDay>, BadDay, List<BadDay>> integrator() {
return Integrator.ofGreedy((currentStreak, nextDay, downstream) -> {
if (currentStreak.isEmpty()) {
// initial streak
currentStreak.add(nextDay);
return true;
}

BadDay previousBadDay = currentStreak.getLast();
boolean isConsecutive = nextDay.date().minusDays(1).equals(previousBadDay.date());
if (isConsecutive) {
// streak continues
currentStreak.add(nextDay);
return true;
}

// streak is broken - emit the current streak and start a new one
downstream.push(List.copyOf(currentStreak));
currentStreak.clear();
currentStreak.add(nextDay);
return true;
});
}

@Override
public BiConsumer<List<BadDay>, Downstream<? super List<BadDay>>> finisher() {
// emit the last streak if we have a running one
return (currentStreak, downstream) -> {
if (!currentStreak.isEmpty()) {
downstream.push(List.copyOf(currentStreak));
}
};
}
}
3 changes: 3 additions & 0 deletions OddJava/src/main/java/com/oddball/challenges/BadWeek.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.oddball.challenges;

public record BadWeek(String userName, int numberOfBadDays) {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@SpringBootApplication
public class OddJavaApp {
public static void main(String[] args) {
static void main(String[] args) {
SpringApplication.run(OddJavaApp.class, args);
}
}
29 changes: 12 additions & 17 deletions OddJava/src/main/java/com/oddball/challenges/mood/Mood.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package com.oddball.challenges.mood;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Date;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "moods")
@Table(
name = "moods",
indexes = {
@Index(name = "idx_mood_date", columnList = "date"),
@Index(name = "idx_mood_user", columnList = "userId"),
}
)
public class Mood {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -26,18 +28,11 @@ public class Mood {
private long userId;

@Column(name = "date", columnDefinition = "date")
private Date date;
private LocalDate date;

@Column(name = "mood", length = 1)
private int mood;

@Column(name = "description", length = 255)
@Column(name = "description")
private String description;

Mood(long userId, Date date, int mood, String description) {
this.userId = userId;
this.date = date;
this.mood = mood;
this.description = description;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
package com.oddball.challenges.mood;

import com.oddball.challenges.BadDay;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;

@Repository
public interface MoodRepository extends CrudRepository<Mood, Long> {

@Query("""
select new com.oddball.challenges.BadDay(m.userId, u.userName, m.date)
from Mood m
join User u on m.userId = u.id
where m.mood in (1, 2)
and (:startDate is null or m.date >= :startDate)
and (:endDate is null or m.date <= :endDate)
""")
List<BadDay> getBadMoodDays(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);

}

Loading