Skip to content

Commit e265db9

Browse files
elliotgnnSheng WangXMJ3083u7713884XingyuDu2025
authored
Automatic Grouping By Date (#14169)
* groups: add model classes: AutomaticDateGroup + DateGroup + some basic tests currently only group based on year not months * model(groups): adding granularity to to model so it can now group by year following month and day, changing AutomaticDateGroup/DateGroup+ DateGranularity + tests (year/month/day) * Fix formatting/checkstyle, apply OpenRewrite, add CHANGELOG * Match OpenRewrite/formatting expected by CI * Add serialization support for AutomaticDateGroup - Add AUTOMATIC_DATE_GROUP_ID constant to MetadataSerializationConfiguration - Implement serializeAutomaticDateGroup() in GroupSerializer * Serializes field name * Serializes granularity (YEAR/MONTH/FULL_DATE) - Add AutomaticDateGroup case in serialization switch - Add getField() and getGranularity() methods to AutomaticDateGroup - Fix deepCopy() and hashCode() to include granularity Serialization format: AutomaticDateGroup:name;context;field;granularity;... * Add deserialization support for AutomaticDateGroup - Add AutomaticDateGroup and DateGranularity imports to GroupsParser - Implement automaticDateGroupFromString() method * Parse name, context, field from serialized string * Parse and convert granularity string to DateGranularity enum * Create AutomaticDateGroup with all parameters * Restore group details (color, icon, description) - Add condition check in fromString() to handle AutomaticDateGroup This enables AutomaticDateGroup to be loaded from .bib files, completing the save/load cycle with serialization. * Add comprehensive tests for AutomaticDateGroup serialization Serialization tests (GroupSerializerTest.java): - Test YEAR granularity serialization - Test MONTH granularity serialization - Test serialization with color, icon, and description - Verify format: AutomaticDateGroup:name;context;field;granularity;... Deserialization tests (GroupsParserTest.java): - Test parsing YEAR granularity - Test parsing MONTH granularity - Test parsing FULL_DATE granularity - Test parsing with color, icon, and description - Verify correct object reconstruction from string Total: 7 new test cases covering all granularity types and edge cases. * Fix: Add missing DateGranularity import to GroupSerializer * Fix: Remove unused DateGranularity import from GroupSerializer Checkstyle reported DateGranularity as an unused import because we only use it through method return type inference (getGranularity().name()). The import is not needed since we don't declare any variables of this type. * Add JavaDoc comments to DateGranularity and DateGroup - Document DateGranularity enum values - Add class-level documentation for DateGroup - Ensures these files are detected as changed files in CI for JBang testing * Add UI components for automatic year groups * Add Date group functionality for automatic grouping * Fix the error popping up when right-clicking the group. * Remove obsolete localization keys from JabRef_en.properties * Clean up redundant comments in GroupDialogViewModel and GroupNodeViewModel * test bug fix BibtexParserTest.java:1453 * fix jbang * turn it back to original case * Fix submodules * Fix submodules --------- Co-authored-by: Sheng Wang <[email protected]> Co-authored-by: Xu <[email protected]> Co-authored-by: Xingyu <[email protected]> Co-authored-by: XingyuDu2025 <[email protected]> Co-authored-by: Christoph <[email protected]>
1 parent f589c16 commit e265db9

File tree

16 files changed

+569
-0
lines changed

16 files changed

+569
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1111

1212
### Added
1313

14+
- We added automatic date-based groups that create year/month/day subgroups from an entry’s date fields. [#10822](https://github.com/JabRef/jabref/issues/10822)
15+
1416
### Changed
1517

1618
### Fixed

jabgui/src/main/java/org/jabref/gui/groups/GroupDialogView.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@
4343
import org.jabref.logic.help.HelpFile;
4444
import org.jabref.logic.l10n.Localization;
4545
import org.jabref.model.database.BibDatabaseContext;
46+
import org.jabref.model.entry.field.Field;
47+
import org.jabref.model.entry.field.FieldFactory;
4648
import org.jabref.model.groups.AbstractGroup;
49+
import org.jabref.model.groups.DateGranularity;
4750
import org.jabref.model.groups.GroupHierarchyType;
4851
import org.jabref.model.groups.GroupTreeNode;
4952
import org.jabref.model.search.SearchFlags;
@@ -101,6 +104,11 @@ public class GroupDialogView extends BaseDialog<AbstractGroup> {
101104

102105
@FXML private TextField texGroupFilePath;
103106

107+
@FXML private RadioButton dateRadioButton;
108+
@FXML private ComboBox<Field> dateGroupFieldCombo;
109+
@FXML private ComboBox<DateGranularity> dateGroupOptionCombo;
110+
@FXML private CheckBox dateGroupIncludeEmpty;
111+
104112
private final EnumMap<GroupHierarchyType, String> hierarchyText = new EnumMap<>(GroupHierarchyType.class);
105113
private final EnumMap<GroupHierarchyType, String> hierarchyToolTip = new EnumMap<>(GroupHierarchyType.class);
106114

@@ -222,6 +230,16 @@ public void initialize() {
222230

223231
texGroupFilePath.textProperty().bindBidirectional(viewModel.texGroupFilePathProperty());
224232

233+
// Date Group bindings
234+
dateRadioButton.selectedProperty().bindBidirectional(viewModel.dateRadioButtonSelectedProperty());
235+
dateGroupFieldCombo.valueProperty().bindBidirectional(viewModel.dateGroupFieldProperty());
236+
dateGroupOptionCombo.valueProperty().bindBidirectional(viewModel.dateGroupOptionProperty());
237+
dateGroupIncludeEmpty.selectedProperty().bindBidirectional(viewModel.dateGroupIncludeEmptyProperty());
238+
239+
// Initialize Date Group ComboBoxes
240+
dateGroupFieldCombo.setItems(FXCollections.observableArrayList(FieldFactory.getDateFields()));
241+
dateGroupOptionCombo.setItems(FXCollections.observableArrayList(DateGranularity.values()));
242+
225243
validationVisualizer.setDecoration(new IconValidationDecorator());
226244
Platform.runLater(() -> {
227245
validationVisualizer.initVisualization(viewModel.nameValidationStatus(), nameField);

jabgui/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@
3939
import org.jabref.model.database.BibDatabase;
4040
import org.jabref.model.database.BibDatabaseContext;
4141
import org.jabref.model.entry.Keyword;
42+
import org.jabref.model.entry.field.Field;
4243
import org.jabref.model.entry.field.FieldFactory;
44+
import org.jabref.model.entry.field.StandardField;
4345
import org.jabref.model.groups.AbstractGroup;
46+
import org.jabref.model.groups.AutomaticDateGroup;
4447
import org.jabref.model.groups.AutomaticGroup;
4548
import org.jabref.model.groups.AutomaticKeywordGroup;
4649
import org.jabref.model.groups.AutomaticPersonsGroup;
50+
import org.jabref.model.groups.DateGranularity;
4751
import org.jabref.model.groups.ExplicitGroup;
4852
import org.jabref.model.groups.GroupHierarchyType;
4953
import org.jabref.model.groups.GroupTreeNode;
@@ -98,6 +102,12 @@ public class GroupDialogViewModel {
98102

99103
private final StringProperty texGroupFilePathProperty = new SimpleStringProperty("");
100104

105+
// Date Group Properties
106+
private final BooleanProperty dateRadioButtonSelectedProperty = new SimpleBooleanProperty();
107+
private final ObjectProperty<Field> dateGroupFieldProperty = new SimpleObjectProperty<>();
108+
private final ObjectProperty<DateGranularity> dateGroupOptionProperty = new SimpleObjectProperty<>();
109+
private final BooleanProperty dateGroupIncludeEmptyProperty = new SimpleBooleanProperty();
110+
101111
private Validator nameValidator;
102112
private Validator nameContainsDelimiterValidator;
103113
private Validator sameNameValidator;
@@ -376,6 +386,13 @@ public AbstractGroup resultConverter(ButtonType button) {
376386
currentDatabase.getMetaData(),
377387
preferences.getFilePreferences().getUserAndHost()
378388
);
389+
} else if (Boolean.TRUE.equals(dateRadioButtonSelectedProperty.getValue())) {
390+
resultingGroup = new AutomaticDateGroup(
391+
groupName,
392+
groupHierarchySelectedProperty.getValue(),
393+
dateGroupFieldProperty.getValue(),
394+
dateGroupOptionProperty.getValue()
395+
);
379396
}
380397

381398
if (resultingGroup != null) {
@@ -413,6 +430,11 @@ public void setValues() {
413430
typeExplicitProperty.setValue(true);
414431
groupHierarchySelectedProperty.setValue(preferences.getGroupsPreferences().getDefaultHierarchicalContext());
415432
autoGroupKeywordsOptionProperty.setValue(Boolean.TRUE);
433+
434+
// Initialize Date Group defaults
435+
dateGroupFieldProperty.setValue(StandardField.DATE);
436+
dateGroupOptionProperty.setValue(DateGranularity.YEAR);
437+
dateGroupIncludeEmptyProperty.setValue(false);
416438
} else {
417439
nameProperty.setValue(editedGroup.getName());
418440
colorUseProperty.setValue(editedGroup.getColor().isPresent());
@@ -458,6 +480,12 @@ public void setValues() {
458480
AutomaticPersonsGroup group = (AutomaticPersonsGroup) editedGroup;
459481
autoGroupPersonsOptionProperty.setValue(Boolean.TRUE);
460482
autoGroupPersonsFieldProperty.setValue(group.getField().getName());
483+
} else if (editedGroup.getClass() == AutomaticDateGroup.class) {
484+
AutomaticDateGroup group = (AutomaticDateGroup) editedGroup;
485+
dateRadioButtonSelectedProperty.setValue(Boolean.TRUE);
486+
dateGroupFieldProperty.setValue(group.getField());
487+
dateGroupOptionProperty.setValue(group.getGranularity());
488+
dateGroupIncludeEmptyProperty.setValue(false);
461489
}
462490
} else if (editedGroup.getClass() == TexGroup.class) {
463491
typeTexProperty.setValue(true);
@@ -651,6 +679,23 @@ public StringProperty texGroupFilePathProperty() {
651679
return texGroupFilePathProperty;
652680
}
653681

682+
// Date Group Property Getters
683+
public BooleanProperty dateRadioButtonSelectedProperty() {
684+
return dateRadioButtonSelectedProperty;
685+
}
686+
687+
public ObjectProperty<Field> dateGroupFieldProperty() {
688+
return dateGroupFieldProperty;
689+
}
690+
691+
public ObjectProperty<DateGranularity> dateGroupOptionProperty() {
692+
return dateGroupOptionProperty;
693+
}
694+
695+
public BooleanProperty dateGroupIncludeEmptyProperty() {
696+
return dateGroupIncludeEmptyProperty;
697+
}
698+
654699
private boolean groupOrSubgroupIsSearchGroup(GroupTreeNode groupTreeNode) {
655700
if (groupTreeNode.getGroup() instanceof SearchGroup) {
656701
return true;

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.jabref.model.entry.BibEntry;
3737
import org.jabref.model.groups.AbstractGroup;
3838
import org.jabref.model.groups.AllEntriesGroup;
39+
import org.jabref.model.groups.AutomaticDateGroup;
3940
import org.jabref.model.groups.AutomaticGroup;
4041
import org.jabref.model.groups.AutomaticKeywordGroup;
4142
import org.jabref.model.groups.AutomaticPersonsGroup;
@@ -461,6 +462,8 @@ public boolean canAddEntriesIn() {
461462
return false;
462463
} else if (group instanceof TexGroup) {
463464
return false;
465+
} else if (group instanceof AutomaticDateGroup) {
466+
return false;
464467
} else {
465468
throw new UnsupportedOperationException("canAddEntriesIn method not yet implemented in group: " + group.getClass().getName());
466469
}
@@ -476,6 +479,7 @@ public boolean canBeDragged() {
476479
SearchGroup _,
477480
AutomaticKeywordGroup _,
478481
AutomaticPersonsGroup _,
482+
AutomaticDateGroup _,
479483
TexGroup _ ->
480484
true;
481485
case KeywordGroup _ ->
@@ -503,6 +507,7 @@ public boolean canAddGroupsIn() {
503507
true;
504508
case AutomaticKeywordGroup _,
505509
AutomaticPersonsGroup _,
510+
AutomaticDateGroup _,
506511
SmartGroup _ ->
507512
false;
508513
case KeywordGroup _ ->
@@ -528,6 +533,7 @@ public boolean canRemove() {
528533
SearchGroup _,
529534
AutomaticKeywordGroup _,
530535
AutomaticPersonsGroup _,
536+
AutomaticDateGroup _,
531537
TexGroup _ ->
532538
true;
533539
case KeywordGroup _ ->
@@ -553,6 +559,7 @@ public boolean isEditable() {
553559
SearchGroup _,
554560
AutomaticKeywordGroup _,
555561
AutomaticPersonsGroup _,
562+
AutomaticDateGroup _,
556563
TexGroup _ ->
557564
true;
558565
case KeywordGroup _ ->

jabgui/src/main/resources/org/jabref/gui/groups/GroupDialog.fxml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
<Tooltip text="%Group containing entries cited in a given TeX file"/>
100100
</tooltip>
101101
</RadioButton>
102+
<RadioButton fx:id="dateRadioButton" toggleGroup="$type" wrapText="true"
103+
text="%Date">
104+
<tooltip>
105+
<Tooltip text="%Automatically create groups by date"/>
106+
</tooltip>
107+
</RadioButton>
102108
</VBox>
103109
<Separator orientation="VERTICAL"/>
104110
<StackPane HBox.hgrow="ALWAYS">
@@ -183,6 +189,18 @@
183189
<Button onAction="#texGroupBrowse" text="%Browse" prefHeight="30.0"/>
184190
</HBox>
185191
</VBox>
192+
<VBox visible="${dateRadioButton.selected}" spacing="10.0">
193+
<VBox>
194+
<Label text="%Field to extract date from"/>
195+
<ComboBox fx:id="dateGroupFieldCombo"/>
196+
</VBox>
197+
<VBox>
198+
<Label text="%Date grouping option"/>
199+
<ComboBox fx:id="dateGroupOptionCombo"/>
200+
</VBox>
201+
<CheckBox fx:id="dateGroupIncludeEmpty"
202+
text="%Include entries without date"/>
203+
</VBox>
186204
</StackPane>
187205
</HBox>
188206
</VBox>

jablib/src/main/java/org/jabref/logic/exporter/GroupSerializer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.jabref.logic.util.strings.StringUtil;
99
import org.jabref.model.groups.AbstractGroup;
1010
import org.jabref.model.groups.AllEntriesGroup;
11+
import org.jabref.model.groups.AutomaticDateGroup;
1112
import org.jabref.model.groups.AutomaticGroup;
1213
import org.jabref.model.groups.AutomaticKeywordGroup;
1314
import org.jabref.model.groups.AutomaticPersonsGroup;
@@ -141,6 +142,8 @@ private String serializeGroup(AbstractGroup group) {
141142
serializeAutomaticKeywordGroup(keywordGroup);
142143
case AutomaticPersonsGroup personsGroup ->
143144
serializeAutomaticPersonsGroup(personsGroup);
145+
case AutomaticDateGroup dateGroup ->
146+
serializeAutomaticDateGroup(dateGroup);
144147
case TexGroup texGroup ->
145148
serializeTexGroup(texGroup);
146149
case null ->
@@ -175,6 +178,18 @@ private String serializeAutomaticPersonsGroup(AutomaticPersonsGroup group) {
175178
return sb.toString();
176179
}
177180

181+
private String serializeAutomaticDateGroup(AutomaticDateGroup group) {
182+
StringBuilder sb = new StringBuilder();
183+
sb.append(MetadataSerializationConfiguration.AUTOMATIC_DATE_GROUP_ID);
184+
appendAutomaticGroupDetails(sb, group);
185+
sb.append(StringUtil.quote(group.getField().getName(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR));
186+
sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);
187+
sb.append(StringUtil.quote(group.getGranularity().name(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR));
188+
sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);
189+
appendGroupDetails(sb, group);
190+
return sb.toString();
191+
}
192+
178193
private void appendAutomaticGroupDetails(StringBuilder builder, AutomaticGroup group) {
179194
builder.append(StringUtil.quote(group.getName(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR));
180195
builder.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);

jablib/src/main/java/org/jabref/logic/importer/util/GroupsParser.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import org.jabref.model.entry.field.Field;
1818
import org.jabref.model.entry.field.FieldFactory;
1919
import org.jabref.model.groups.AbstractGroup;
20+
import org.jabref.model.groups.AutomaticDateGroup;
2021
import org.jabref.model.groups.AutomaticKeywordGroup;
2122
import org.jabref.model.groups.AutomaticPersonsGroup;
23+
import org.jabref.model.groups.DateGranularity;
2224
import org.jabref.model.groups.ExplicitGroup;
2325
import org.jabref.model.groups.GroupHierarchyType;
2426
import org.jabref.model.groups.GroupTreeNode;
@@ -118,6 +120,9 @@ public static AbstractGroup fromString(String s, Character keywordSeparator, Fil
118120
if (s.startsWith(MetadataSerializationConfiguration.AUTOMATIC_KEYWORD_GROUP_ID)) {
119121
return automaticKeywordGroupFromString(s);
120122
}
123+
if (s.startsWith(MetadataSerializationConfiguration.AUTOMATIC_DATE_GROUP_ID)) {
124+
return automaticDateGroupFromString(s);
125+
}
121126
if (s.startsWith(MetadataSerializationConfiguration.TEX_GROUP_ID)) {
122127
return texGroupFromString(s, fileMonitor, metaData, userAndHost);
123128
}
@@ -165,6 +170,23 @@ private static AbstractGroup automaticPersonsGroupFromString(String string) {
165170
return newGroup;
166171
}
167172

173+
private static AbstractGroup automaticDateGroupFromString(String string) {
174+
if (!string.startsWith(MetadataSerializationConfiguration.AUTOMATIC_DATE_GROUP_ID)) {
175+
throw new IllegalArgumentException("AutomaticDateGroup cannot be created from \"" + string + "\".");
176+
}
177+
QuotedStringTokenizer tok = new QuotedStringTokenizer(string.substring(MetadataSerializationConfiguration.AUTOMATIC_DATE_GROUP_ID
178+
.length()), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR);
179+
180+
String name = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR);
181+
GroupHierarchyType context = GroupHierarchyType.getByNumberOrDefault(Integer.parseInt(tok.nextToken()));
182+
Field field = FieldFactory.parseField(StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR));
183+
String granularityString = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR);
184+
DateGranularity granularity = DateGranularity.valueOf(granularityString);
185+
AutomaticDateGroup newGroup = new AutomaticDateGroup(name, context, field, granularity);
186+
addGroupDetails(tok, newGroup);
187+
return newGroup;
188+
}
189+
168190
private static AbstractGroup automaticKeywordGroupFromString(String string) {
169191
if (!string.startsWith(MetadataSerializationConfiguration.AUTOMATIC_KEYWORD_GROUP_ID)) {
170192
throw new IllegalArgumentException("KeywordGroup cannot be created from \"" + string + "\".");

jablib/src/main/java/org/jabref/logic/util/MetadataSerializationConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ public class MetadataSerializationConfiguration {
7474
*/
7575
public static final String TEX_GROUP_ID = "TexGroup:";
7676

77+
/**
78+
* Identifier for AutomaticDateGroup.
79+
*/
80+
public static final String AUTOMATIC_DATE_GROUP_ID = "AutomaticDateGroup:";
81+
7782
private MetadataSerializationConfiguration() {
7883
}
7984
}

jablib/src/main/java/org/jabref/model/entry/field/FieldFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ public static Set<Field> getPersonNameFields() {
193193
return getFieldsFiltered(field -> field.getProperties().contains(FieldProperty.PERSON_NAMES));
194194
}
195195

196+
public static Set<Field> getDateFields() {
197+
return getFieldsFiltered(field -> field.getProperties().contains(FieldProperty.DATE));
198+
}
199+
196200
private static Set<Field> getFieldsFiltered(Predicate<Field> selector) {
197201
return getAllFields().stream()
198202
.filter(selector)

0 commit comments

Comments
 (0)