Skip to content

Commit

Permalink
#16 Autocomplete for projects
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada committed Nov 28, 2020
1 parent bf41b64 commit 2073468
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package org.itsallcode.whiterabbit.jfxui.testutil.model;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import java.util.ArrayList;
import java.util.List;

import javafx.scene.control.IndexedCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.VirtualFlow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.itsallcode.whiterabbit.jfxui.JavaFxUtil;
Expand All @@ -14,10 +12,11 @@
import org.testfx.api.FxRobot;
import org.testfx.assertions.api.Assertions;

import javafx.scene.control.IndexedCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.VirtualFlow;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

public class JavaFxTable<T>
{
Expand Down Expand Up @@ -72,7 +71,6 @@ public IndexedCell<T> getTableRow(final int rowIndex)
.map(VirtualFlow.class::cast)
.findFirst().orElseThrow();
assertThat(virtualFlow.getCellCount()).as("row count of " + virtualFlow).isGreaterThan(rowIndex);
System.out.println("Found flow " + virtualFlow + " with cells# " + virtualFlow.getCellCount());
final IndexedCell<T> row = JavaFxUtil.runOnFxApplicationThread(() -> virtualFlow.getCell(rowIndex));
LOG.debug("Got row #{} of {}: {}", rowIndex, virtualFlow, row);
return row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import java.time.LocalDate;
import java.time.Period;
import java.util.*;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.Collections.emptyList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.*;

public class AutocompleteService
{
Expand Down Expand Up @@ -52,29 +54,38 @@ private List<String> getDayComments()

private List<String> getActivityComments()
{
return getLatestDays().stream()
.map(DayRecord::activities)
.map(DayActivities::getAll)
.flatMap(List::stream)
return getActivities()
.map(Activity::getComment)
.filter(Objects::nonNull)
.filter(comment -> !comment.isBlank())
.distinct()
.collect(toList());
}

private Stream<Activity> getActivities()
{
return getLatestDays().stream()
.map(DayRecord::activities)
.map(DayActivities::getAll)
.flatMap(List::stream);
}

private List<DayRecord> getLatestDays()
{
final LocalDate maxAge = clockService.getCurrentDate().minus(MAX_AGE);
return storage.getLatestDays(maxAge);
}

private AutocompleteEntrySupplier autocompleter(Collection<String> allEntries)
AutocompleteEntrySupplier autocompleter(Collection<String> allEntries)
{
LOG.debug("Creating autocompleter for {} entries: {}", allEntries.size(), allEntries);
final Map<String, List<String>> lowerCaseIndex = allEntries.stream().collect(groupingBy(String::toLowerCase));
final SortedSet<String> lowerCaseValues = new TreeSet<>(lowerCaseIndex.keySet());
return currentText -> {
if (currentText == null || currentText.isBlank())
{
return emptyList();
}
final SortedSet<String> lowerCaseMatches = lowerCaseValues.subSet(currentText.toLowerCase(),
currentText.toLowerCase() + Character.MAX_VALUE);
return lowerCaseMatches.stream().map(lowerCaseIndex::get).flatMap(List::stream).collect(toList());
Expand All @@ -83,6 +94,21 @@ private AutocompleteEntrySupplier autocompleter(Collection<String> allEntries)

public Optional<Project> getSuggestedProject()
{
return Optional.empty();
final List<Project> projects = getActivities().map(Activity::getProject)
.filter(Objects::nonNull)
.collect(toList());
final Map<String, List<Project>> groupedProjects = projects.stream()
.filter(Objects::nonNull)
.collect(groupingBy(Project::getProjectId));
final Map<String, Long> frequencyMap = projects.stream()
.map(Project::getProjectId)
.collect(groupingBy(identity(), counting()));
final Optional<Project> mostFrequentlyUsedProject = frequencyMap
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.map(projectId -> groupedProjects.get(projectId).get(0));
LOG.debug("Project frequency: {}, most frequently: {}", frequencyMap, mostFrequentlyUsedProject);
return mostFrequentlyUsedProject;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.itsallcode.whiterabbit.logic.autocomplete;

import org.itsallcode.whiterabbit.logic.model.Activity;
import org.itsallcode.whiterabbit.logic.model.DayActivities;
import org.itsallcode.whiterabbit.logic.model.DayRecord;
import org.itsallcode.whiterabbit.logic.service.ClockService;
import org.itsallcode.whiterabbit.logic.service.project.Project;
import org.itsallcode.whiterabbit.logic.storage.CachingStorage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AutocompleteServiceTest
{
@Mock
CachingStorage storageMock;
@Mock
private ClockService clockServiceMock;

private AutocompleteService autocompleteService;

@BeforeEach
void setUp()
{
autocompleteService = new AutocompleteService(storageMock, clockServiceMock);
}

@Test
void dayCommentAutocompleter()
{
simulateDays(dayRecordsWithComments(null, "", "Comment A", "Comment B"));
assertThat(autocompleteService.dayCommentAutocompleter().getEntries("comm"))
.containsExactly("Comment A", "Comment B");
}

@Test
void activityCommentAutocompleter()
{
simulateDays(createDayRecordsWithActivityComments(null, "", "Comment A", "Comment B"));
assertThat(autocompleteService.activityCommentAutocompleter().getEntries("comm"))
.containsExactly("Comment A", "Comment B");
}

@ParameterizedTest(name = "[{index}] available values {0}, search text ''{1}''")
@ArgumentsSource(AutocompleterArgumentsProvider.class)
void autocompleter(List<String> availableEntries, String searchText, List<String> expectedResult)
{
assertThat(autocompleteService.autocompleter(availableEntries).getEntries(searchText))
.as("autocomplete for available values " + availableEntries + " and search text '" + searchText + "'")
.containsExactly(expectedResult.toArray(new String[0]));
}

private static class AutocompleterArgumentsProvider implements ArgumentsProvider
{
@Override
public Stream<Arguments> provideArguments(ExtensionContext context) throws Exception
{
return Stream.of(
Arguments.of(List.of(), "text", List.of()),
Arguments.of(List.of("text"), null, List.of()),
Arguments.of(List.of("text"), "", List.of()),
Arguments.of(List.of("text"), " ", List.of()),
Arguments.of(List.of("TEXT"), "text", List.of("TEXT")),
Arguments.of(List.of("match", "nomatch"), "ma", List.of("match")),
Arguments.of(List.of("match1", "match2"), "ma", List.of("match1", "match2")),
Arguments.of(List.of("first second"), "fi", List.of("first second")),
Arguments.of(List.of("first second"), "sec", List.of()));
}
}

@Test
void getSuggestedProject_returnsEmptyOptional_whenNoDataAvailable()
{
simulateDays(createDayRecordsWithActivityProjects());
assertThat(autocompleteService.getSuggestedProject()).isEmpty();
}

@Test
void getSuggestedProject_singleProject()
{
simulateDays(createDayRecordsWithActivityProjects("p1"));
assertProjectFound("p1");
}

@Test
void getSuggestedProject_mostFrequentProjectReturned()
{
simulateDays(createDayRecordsWithActivityProjects("p1", "p2", "p2"));
assertProjectFound("p2");
}

private void assertProjectFound(String expectedProjectId)
{
final Optional<Project> suggestedProject = autocompleteService.getSuggestedProject();
assertThat(suggestedProject).isPresent();
assertThat(suggestedProject.get().getProjectId()).isEqualTo(expectedProjectId);
}

private void simulateDays(final List<DayRecord> days)
{
final LocalDate now = LocalDate.of(2020, Month.APRIL, 1);
final LocalDate maxAge = now.minus(Period.ofMonths(2));
when(clockServiceMock.getCurrentDate()).thenReturn(now);
when(storageMock.getLatestDays(maxAge)).thenReturn(days);
}

private List<DayRecord> createDayRecordsWithActivityComments(String... comments)
{
return Arrays.stream(comments).map(this::createDayRecordWithActivityComment).collect(toList());
}

private List<DayRecord> createDayRecordsWithActivityProjects(String... projectIds)
{
return Arrays.stream(projectIds).map(this::createDayRecordWithProject).collect(toList());
}

private DayRecord createDayRecordWithProject(String projectId)
{
final Activity activity = mock(Activity.class);
final Project project = new Project(projectId, "Project " + projectId, null);
when(activity.getProject()).thenReturn(project);
return dayWithActivities(activity);
}

private DayRecord createDayRecordWithActivityComment(String comment)
{
final Activity activity = mock(Activity.class);
when(activity.getComment()).thenReturn(comment);
return dayWithActivities(activity);
}

private DayRecord dayWithActivities(Activity... activityList)
{
final DayActivities activities = mock(DayActivities.class);
when(activities.getAll()).thenReturn(asList(activityList));
final DayRecord dayRecord = mock(DayRecord.class);
when(dayRecord.activities()).thenReturn(activities);
return dayRecord;
}

private List<DayRecord> dayRecordsWithComments(String... comments)
{
return Arrays.stream(comments).map(this::createDayRecord).collect(toList());
}

private DayRecord createDayRecord(String comment)
{
final DayRecord dayRecord = mock(DayRecord.class);
when(dayRecord.getComment()).thenReturn(comment);
return dayRecord;
}
}

0 comments on commit 2073468

Please sign in to comment.