diff --git a/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistory.java b/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistory.java index 65f7c35..92a9cbb 100644 --- a/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistory.java +++ b/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistory.java @@ -4,8 +4,6 @@ import eug.shared.GenericList; import eug.shared.GenericObject; -import eug.shared.HeaderComment; -import eug.shared.InlineComment; import eug.shared.ObjectVariable; import eug.shared.WritableObject; import java.util.Comparator; @@ -35,7 +33,7 @@ private ClausewitzHistory() { } // private static final Pattern DATE_PATTERN = // Pattern.compile("[0-9]{1,4}\\.[0-9]{1,2}\\.[0-9]{1,2}"); - private static boolean isDate(final String str) { + static boolean isDate(final String str) { // As it turns out, a regex is simple but rather slow for this. // Instead, this method uses a handcrafted validator. // return DATE_PATTERN.matcher(str).matches(); @@ -329,108 +327,6 @@ public static boolean isRhsSet( return isSet; } - - /** - * Merges the two history objects, overwriting any parts of existing object - * that the additions match. The final product is then sorted in the usual - * history order (first bare variables, then date objects in order). - *

- * For example, take the object: - *

-     * add_core = SWE
-     * owner = SWE
-     * controller = SWE
-     * culture = swedish
-     * base_tax = 8
-     * 
-     * 1450.1.1 = { base_tax = 10 }
-     * 
- * And the following additions: - *
-     * hre = no
-     * 1420.1.1 = { controller = REB }
-     * 1550.1.1 = { religion = protestant }
-     * 
- * - * After invoking this method, the original object would be updated to: - *
-     * add_core = SWE
-     * owner = SWE
-     * controller = SWE
-     * culture = swedish
-     * base_tax = 8
-     * hre = no
-     * 
-     * 1420.1.1 = { controller = REB }
-     * 1450.1.1 = { base_tax = 10 }
-     * 1550.1.1 = { religion = protestant }
-     * 
- * @param existing the object to add new parts into - * @param additions the new parts to add into the existing object - */ - public static void mergeHistObjects(GenericObject existing, GenericObject additions) { - for (WritableObject wo : additions.getAllWritable()) { - if (wo instanceof ObjectVariable) { - ObjectVariable newVar = (ObjectVariable) wo; - if (!mergeVariable(existing, newVar)) { - existing.addVariable(newVar); - } - } else if (wo instanceof GenericList) { - GenericList newList = (GenericList) wo; - GenericList oldList = existing.getList(newList.getName()); - if (oldList != null) { - // merging lists seems wrong, so let's just overwrite it - oldList.clear(); - oldList.addAll(newList); - oldList.setHeaderComment(newList.getHeaderComment()); - oldList.setInlineComment(newList.getInlineComment()); - } else { - existing.addList(newList); - } - } else if (wo instanceof GenericObject) { - GenericObject newObj = (GenericObject) wo; - GenericObject oldObj = existing.getChild(newObj.name); - if (oldObj != null) { - mergeHistObjects(oldObj, newObj); - } else { - existing.addChild(newObj); - } - } else if (wo instanceof HeaderComment) { - existing.addGeneralComment(((HeaderComment) wo).getComment(), true); - } else if (wo instanceof InlineComment) { - existing.addGeneralComment(((InlineComment) wo).getComment(), false); - } - } - - if (!"".equals(additions.getHeadComment())) - existing.setHeadComment(additions.getHeadComment()); - if (!"".equals(additions.getInlineComment())) - existing.setInlineComment(additions.getInlineComment()); - - existing.getAllWritable().sort(new HistoryObjectComparator()); - } - - private static boolean mergeVariable(GenericObject existing, ObjectVariable newVar) { - // hack: discovered_by is frequently used in history files and - // is not unique, so DON'T merge it. - if (newVar.varname.equalsIgnoreCase("discovered_by")) - return false; - - // instead of using setString, we do the loop ourselves so we have access to the original ObjectVariable to add any comments - for (ObjectVariable oldVar : existing.values) { - if (oldVar.varname.equalsIgnoreCase(newVar.varname)) { - // copy everything over - // could merge the comments instead of copying, but that would likely result in odd outcomes - if (!"".equals(newVar.getHeadComment())) - oldVar.setHeadComment(newVar.getHeadComment()); - oldVar.setValue(newVar.getValue()); - if (!"".equals(newVar.getInlineComment())) - oldVar.setInlineComment(newVar.getInlineComment()); - return true; - } - } - return false; - } public static final class DateComparator implements Comparator { @@ -488,7 +384,7 @@ public boolean isBefore(final String date1, final String date2) { } } - private static class HistoryObjectComparator implements Comparator { + static class HistoryObjectComparator implements Comparator { private final DateComparator dateComparator; diff --git a/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistoryMergeTool.java b/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistoryMergeTool.java new file mode 100644 index 0000000..2cc108e --- /dev/null +++ b/EugFile_specific/src/eug/specific/clausewitz/ClausewitzHistoryMergeTool.java @@ -0,0 +1,358 @@ +package eug.specific.clausewitz; + +import eug.shared.GenericList; +import eug.shared.GenericObject; +import eug.shared.HeaderComment; +import eug.shared.InlineComment; +import eug.shared.ObjectVariable; +import eug.shared.WritableObject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * + * @author Michael + */ +public class ClausewitzHistoryMergeTool { + + + public enum MergeMode { + + /** + * Mode to merge the two objects, only adding and never overwriting any + * parts of the existing object that the additions match. + *

+ * For example, take the object: + *

+         * add_core = SWE
+         * owner = SWE
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * 
+         * 1450.1.1 = { base_tax = 10 }
+         * 
+ * And the following additions: + *
+         * add_core = DAN
+         * 1420.1.1 = { controller = REB }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ * + * After invoking this method, the original object would be updated to: + *
+         * add_core = SWE
+         * owner = SWE
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * add_core = DAN
+         * 
+         * 1420.1.1 = { controller = REB }
+         * 1450.1.1 = { base_tax = 10 }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ */ + ADD, + + /** + * Merges the two history objects, overwriting any parts of existing object + * that the additions match. The final product is then sorted in the usual + * history order (first bare variables, then date objects in order). + *

+ * For example, take the object: + *

+         * add_core = SWE
+         * owner = SWE
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * 
+         * 1450.1.1 = { base_tax = 10 }
+         * 
+ * And the following additions: + *
+         * hre = no
+         * 1420.1.1 = { controller = REB }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ * + * After invoking this method, the original object would be updated to: + *
+         * add_core = SWE
+         * owner = SWE
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * hre = no
+         * 
+         * 1420.1.1 = { controller = REB }
+         * 1450.1.1 = { base_tax = 10 }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ */ + OVERWRITE, + + /** + * Merges the two objects, overwriting any parts of the existing object + * that the additions match, except for ObjectVariables whose name is + * in the set of elements not to merge. + *

+ * For example, take the object: + *

+         * add_core = SWE
+         * owner = SWE
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * 
+         * 1450.1.1 = { base_tax = 10 }
+         * 
+ * And the following additions: + *
+         * add_core = DAN
+         * owner = DAN
+         * 1420.1.1 = { controller = REB }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ * + * Assuming {@link #initAutoMergeList} has been called with an EU4 data + * source, the value "owner" will be automatically merged (since it only + * ever appears once in the same block) but "add_core" will not. + * So after invoking this method, the original object would be updated to: + *
+         * add_core = SWE
+         * owner = DAN
+         * controller = SWE
+         * culture = swedish
+         * base_tax = 8
+         * add_core = DAN
+         * 
+         * 1420.1.1 = { controller = REB }
+         * 1450.1.1 = { base_tax = 10 }
+         * 1550.1.1 = { religion = protestant }
+         * 
+ * @see initAutoMergeList + */ + AUTO, + DELETE + } + + /** + * Stores all strings that we detect multiples of in province histories, + * so that when using automatic merge mode, we don't overwrite variables + * whose names are in the set. + * Stored canonically lower-case so we don't have to check equalsIgnoreCase() + * on each element, which defeats the purpose of using a set rather than a list. + */ + private final Set elementsToNotMerge; + + public ClausewitzHistoryMergeTool() { + elementsToNotMerge = new HashSet<>(); + } + + /** + * Reads province history files from the given data source in an attempt to + * deduce which items may appear more than once and which can only appear + * once. This information is used when merging objects with + * {@link MergeMode.AUTO}. + * @param data + */ + public void initAutoMergeList(ClausewitzDataSource data) { + for (int i = 1; ; i++) { + GenericObject hist = data.getProvinceHistory(i); + if (hist == null) + break; + + checkDuplicateValues(hist); + for (GenericObject child : hist.children) + checkDuplicateValues(child); + } + } + + private void checkDuplicateValues(GenericObject obj) { + Set vals = new HashSet<>(); + for (ObjectVariable v : obj.values) { + String name = v.varname.toLowerCase(); + if (vals.contains(name)) + elementsToNotMerge.add(name); + vals.add(name); + } + } + + public void initAutoMergeList(Collection notToMerge) { + elementsToNotMerge.addAll(notToMerge); + } + + public void mergeHistObjects(GenericObject existing, GenericObject modifications) { + mergeHistObjects(existing, modifications, MergeMode.OVERWRITE); + } + + public void mergeHistObjects(GenericObject existing, GenericObject modifications, MergeMode mode) { + switch(mode) { + case DELETE: + deleteFrom(existing, modifications); + break; + case ADD: + case AUTO: + case OVERWRITE: + default: + doMerge(existing, modifications, mode); + break; + } + } + + private void doMerge(GenericObject existing, GenericObject additions, MergeMode mode) { + for (WritableObject wo : additions.getAllWritable()) { + if (wo instanceof ObjectVariable) { + ObjectVariable newVar = (ObjectVariable) wo; + if (!mergeVariable(existing, newVar, mode)) { + existing.addVariable(newVar); + } + } else if (wo instanceof GenericList) { + GenericList newList = (GenericList) wo; + GenericList oldList = existing.getList(newList.getName()); + if (oldList != null) { + // merging lists seems wrong, so let's just overwrite it + oldList.clear(); + oldList.addAll(newList); + oldList.setHeaderComment(newList.getHeaderComment()); + oldList.setInlineComment(newList.getInlineComment()); + } else { + existing.addList(newList); + } + } else if (wo instanceof GenericObject) { + GenericObject newObj = (GenericObject) wo; + GenericObject oldObj = existing.getChild(newObj.name); + if (oldObj != null) { + doMerge(oldObj, newObj, mode); // recursion + } else { + existing.addChild(newObj); + } + } else if (wo instanceof HeaderComment) { + existing.addGeneralComment(((HeaderComment) wo).getComment(), true); + } else if (wo instanceof InlineComment) { + existing.addGeneralComment(((InlineComment) wo).getComment(), false); + } + } + + if (!"".equals(additions.getHeadComment())) + existing.setHeadComment(additions.getHeadComment()); + if (!"".equals(additions.getInlineComment())) + existing.setInlineComment(additions.getInlineComment()); + + existing.getAllWritable().sort(new ClausewitzHistory.HistoryObjectComparator()); + } + + + /** + * Merges the two objects, deleting any parts of the existing object that + * the modifications match. A date object will not be deleted unless all + * its contents have been deleted. + * @param existing + * @param modifications + */ + private void deleteFrom(GenericObject existing, GenericObject modifications) { + List deletedObjects = new ArrayList<>(); + List deletedLists = new ArrayList<>(); + + for (WritableObject wo : modifications.getAllWritable()) { + if (wo instanceof ObjectVariable) { + ObjectVariable varToDelete = (ObjectVariable) wo; + deleteVariable(existing, varToDelete); + } else if (wo instanceof GenericList) { + GenericList newList = (GenericList) wo; + GenericList oldList = existing.getList(newList.getName()); + if (oldList != null) { + if (oldList.deleteAll(newList) && oldList.size() == 0) + deletedLists.add(oldList); + } else { + existing.addList(newList); + } + } else if (wo instanceof GenericObject) { + GenericObject newObj = (GenericObject) wo; + GenericObject oldObj = existing.getChild(newObj.name); + if (oldObj != null) { + deleteFrom(oldObj, newObj); // recursion + if (oldObj.isEmpty()) + deletedObjects.add(oldObj); + } + } + } + + for (GenericObject obj : deletedObjects) { + existing.getAllWritable().remove(obj); + existing.children.remove(obj); + } + + for (GenericList list : deletedLists) { + existing.getAllWritable().remove(list); + existing.lists.remove(list); + } + + // unlike doMerge(), we don't care about any comments on the new object + // nor do we need to sort the history + } + + + /** + * Merges the variable into the object, returning true if successful and + * false if the variable should simply be added. + * @param existing + * @param newVar + * @param mode + * @return + */ + private boolean mergeVariable(GenericObject existing, ObjectVariable newVar, MergeMode mode) { + switch(mode) { + case ADD: + case DELETE: // should never happen + return false; + case AUTO: + // we keep a list of variable names that should NOT be merged by default + // so check that first + if (!elementsToNotMerge.isEmpty() && elementsToNotMerge.contains(newVar.varname.toLowerCase())) + return false; + // otherwise merge + break; + case OVERWRITE: + default: + // always merge if possible + break; + } + + // instead of using setString, we do the loop ourselves so we have access to the original ObjectVariable to add any comments + for (ObjectVariable oldVar : existing.values) { + if (oldVar.varname.equalsIgnoreCase(newVar.varname)) { + // copy everything over + // could merge the comments instead of copying, but that would likely result in odd outcomes + if (!"".equals(newVar.getHeadComment())) + oldVar.setHeadComment(newVar.getHeadComment()); + oldVar.setValue(newVar.getValue()); + if (!"".equals(newVar.getInlineComment())) + oldVar.setInlineComment(newVar.getInlineComment()); + return true; + } + } + return false; + } + + private void deleteVariable(GenericObject existing, ObjectVariable varToDelete) { + // we must loop through the object to see if there is a variable with + // an EXACT match of both name and value + + ObjectVariable found = null; + for (ObjectVariable oldVar : existing.values) { + if (oldVar.varname.equalsIgnoreCase(varToDelete.varname) + && oldVar.getValue().equalsIgnoreCase(varToDelete.getValue())) { + found = oldVar; + break; + } + } + existing.getAllWritable().remove(found); + existing.values.remove(found); + } +} diff --git a/EugFile_specific/test/eug/specific/clausewitz/ClausewitzHistoryTest.java b/EugFile_specific/test/eug/specific/clausewitz/ClausewitzHistoryTest.java index 429f2e4..f9618d8 100644 --- a/EugFile_specific/test/eug/specific/clausewitz/ClausewitzHistoryTest.java +++ b/EugFile_specific/test/eug/specific/clausewitz/ClausewitzHistoryTest.java @@ -3,6 +3,7 @@ import eug.parser.EUGFileIO; import eug.shared.GenericObject; +import java.util.Arrays; import junit.framework.TestCase; import org.junit.Test; @@ -18,17 +19,26 @@ public class ClausewitzHistoryTest extends TestCase { "controller = SWE\n" + "culture = swedish\n" + "base_tax = 8\n" + + "discovered_by = latin\n" + "\n" + "1450.1.1 = { base_tax = 10 }\n" + "1550.1.1 = { base_manpower = 5 }"; private static final String ADDITIONAL_OBJECT = - "#no hre\n" + - "hre = no\n" + + "add_core = DAN\n" + + "owner = DAN\n" + + "hre = no # comment\n" + + "discovered_by = eastern\n" + "1420.1.1 = { controller = REB } # rebellion\n" + "1550.1.1 = { religion = protestant }\n" + "1450.1.1 = { base_tax = 12 }"; + private static final String OBJECT_TO_DELETE = + "testlist = { AAA }\n" + + "testlist2 = { AAA }\n" + + "discovered_by = latin\n" + + "1550.1.1 = { base_manpower = 5 }"; + public ClausewitzHistoryTest(String testName) { super(testName); } @@ -38,13 +48,98 @@ public ClausewitzHistoryTest(String testName) { */ @Test public void testMergeHistObjects() { + String expected = + "add_core = DAN\r\n" + + "owner = DAN\r\n" + + "controller = SWE\r\n" + + "culture = swedish\r\n" + + "base_tax = 8\r\n" + + "discovered_by = eastern\r\n" + + "hre = no # comment\r\n" + + "1420.1.1 = { controller = REB } # rebellion\r\n" + + "1450.1.1 = { base_tax = 12 } \r\n" + + "1550.1.1 = { base_manpower = 5 religion = protestant } "; + + System.out.println("mergeHistObjects"); + GenericObject existing = EUGFileIO.loadFromString(ORIGINAL_OBJECT); + GenericObject additions = EUGFileIO.loadFromString(ADDITIONAL_OBJECT); + + new ClausewitzHistoryMergeTool().mergeHistObjects(existing, additions, ClausewitzHistoryMergeTool.MergeMode.OVERWRITE); + + assertEquals(expected, existing.toString()); + } + + @Test + public void testMergeAddHistObjects() { + String expected = + "add_core = SWE\r\n" + + "owner = SWE\r\n" + + "controller = SWE\r\n" + + "culture = swedish\r\n" + + "base_tax = 8\r\n" + + "discovered_by = latin\r\n" + + "add_core = DAN\r\n" + + "owner = DAN\r\n" + + "hre = no # comment\r\n" + + "discovered_by = eastern\r\n" + + "1420.1.1 = { controller = REB } # rebellion\r\n" + + "1450.1.1 = { base_tax = 10 base_tax = 12 } \r\n" + + "1550.1.1 = { base_manpower = 5 religion = protestant } "; + System.out.println("mergeHistObjects"); GenericObject existing = EUGFileIO.loadFromString(ORIGINAL_OBJECT); GenericObject additions = EUGFileIO.loadFromString(ADDITIONAL_OBJECT); - ClausewitzHistory.mergeHistObjects(existing, additions); + new ClausewitzHistoryMergeTool().mergeHistObjects(existing, additions, ClausewitzHistoryMergeTool.MergeMode.ADD); - System.out.println(existing.toString()); + assertEquals(expected, existing.toString()); } + @Test + public void testAutoMergeHistObjects() { + String expected = + "add_core = SWE\r\n" + + "owner = DAN\r\n" + + "controller = SWE\r\n" + + "culture = swedish\r\n" + + "base_tax = 8\r\n" + + "discovered_by = latin\r\n" + + "add_core = DAN\r\n" + + "hre = no # comment\r\n" + + "discovered_by = eastern\r\n" + + "1420.1.1 = { controller = REB } # rebellion\r\n" + + "1450.1.1 = { base_tax = 12 } \r\n" + + "1550.1.1 = { base_manpower = 5 religion = protestant } "; + + System.out.println("mergeHistObjects"); + GenericObject existing = EUGFileIO.loadFromString(ORIGINAL_OBJECT); + GenericObject additions = EUGFileIO.loadFromString(ADDITIONAL_OBJECT); + + ClausewitzHistoryMergeTool mergeTool = new ClausewitzHistoryMergeTool(); + mergeTool.initAutoMergeList(Arrays.asList("add_core", "discovered_by")); + mergeTool.mergeHistObjects(existing, additions, ClausewitzHistoryMergeTool.MergeMode.AUTO); + + assertEquals(expected, existing.toString()); + } + + @Test + public void testDeleteHistObjects() { + String expected = + "testlist = { BBB }\r\n" + + "add_core = SWE\r\n" + + "owner = SWE\r\n" + + "controller = SWE\r\n" + + "culture = swedish\r\n" + + "base_tax = 8\r\n" + + "1450.1.1 = { base_tax = 10 } "; + + System.out.println("mergeHistObjects"); + GenericObject existing = EUGFileIO.loadFromString("testlist = { AAA BBB } testlist2 = { AAA } " + ORIGINAL_OBJECT); + GenericObject additions = EUGFileIO.loadFromString(OBJECT_TO_DELETE); + + ClausewitzHistoryMergeTool mergeTool = new ClausewitzHistoryMergeTool(); + mergeTool.mergeHistObjects(existing, additions, ClausewitzHistoryMergeTool.MergeMode.DELETE); + + assertEquals(expected, existing.toString()); + } } diff --git a/eugFile/src/eug/shared/GenericList.java b/eugFile/src/eug/shared/GenericList.java index 1ffd6fc..0c96842 100644 --- a/eugFile/src/eug/shared/GenericList.java +++ b/eugFile/src/eug/shared/GenericList.java @@ -101,6 +101,10 @@ public boolean delete(String val) { return list.remove(val); } + public boolean deleteAll(GenericList other) { + return list.removeAll(other.list); + } + public void clear() { list.clear(); }