44import java .util .Arrays ;
55import java .util .Collection ;
66import java .util .Collections ;
7+ import java .util .LinkedHashSet ;
78import java .util .List ;
9+ import java .util .Set ;
10+ import java .util .SortedSet ;
811
912import ai .timefold .solver .core .api .domain .solution .PlanningSolution ;
1013import ai .timefold .solver .core .api .domain .valuerange .CountableValueRange ;
1114import ai .timefold .solver .core .api .domain .valuerange .ValueRange ;
1215import ai .timefold .solver .core .api .domain .valuerange .ValueRangeProvider ;
16+ import ai .timefold .solver .core .api .domain .variable .PlanningListVariable ;
1317import ai .timefold .solver .core .api .domain .variable .PlanningVariable ;
1418import ai .timefold .solver .core .config .util .ConfigUtils ;
1519import ai .timefold .solver .core .impl .domain .common .accessor .MemberAccessor ;
1620import ai .timefold .solver .core .impl .domain .entity .descriptor .EntityDescriptor ;
1721import ai .timefold .solver .core .impl .domain .valuerange .buildin .collection .ListValueRange ;
22+ import ai .timefold .solver .core .impl .domain .valuerange .buildin .collection .SetValueRange ;
1823import ai .timefold .solver .core .impl .domain .variable .descriptor .GenuineVariableDescriptor ;
1924
2025/**
@@ -103,23 +108,35 @@ protected <Value_> ValueRange<Value_> readValueRange(Object bean) {
103108 .formatted (ValueRangeProvider .class .getSimpleName (), memberAccessor , bean , valueRangeObject ));
104109 }
105110 ValueRange <Value_ > valueRange ;
106- if (collectionWrapping || arrayWrapping ) {
107- List <Value_ > list = collectionWrapping ? transformCollectionToUniqueList ((Collection <Value_ >) valueRangeObject )
108- : transformArrayToUniqueList (valueRangeObject );
109- // Don't check the entire list for performance reasons, but do check common pitfalls
110- if (!list .isEmpty () && (list .get (0 ) == null || list .get (list .size () - 1 ) == null )) {
111- throw new IllegalStateException (
112- """
113- The @%s-annotated member (%s) called on bean (%s) must not return a %s (%s) with an element that is null.
114- Maybe remove that null element from the dataset.
115- Maybe use @%s(allowsUnassigned = true) instead."""
116- .formatted (ValueRangeProvider .class .getSimpleName (),
117- memberAccessor , bean ,
118- collectionWrapping ? Collection .class .getSimpleName () : "array" ,
119- list ,
120- PlanningVariable .class .getSimpleName ()));
121- }
111+ if (arrayWrapping ) {
112+ List <Value_ > list = transformArrayToList (valueRangeObject );
113+ assertNullNotPresent (list , bean );
122114 valueRange = new ListValueRange <>(list );
115+ } else if (collectionWrapping ) {
116+ var collection = (Collection <Value_ >) valueRangeObject ;
117+ if (collection instanceof Set <Value_ > set ) {
118+ if (set .contains (null )) {
119+ throw new IllegalStateException ("""
120+ The @%s-annotated member (%s) called on bean (%s) returns a Set (%s) with a null element.
121+ Maybe remove that null element from the dataset.
122+ Maybe use @%s(allowsUnassigned = true) or @%s(allowsUnassignedValues = true) instead."""
123+ .formatted (ValueRangeProvider .class .getSimpleName (), memberAccessor , bean , set ,
124+ PlanningVariable .class .getSimpleName (), PlanningListVariable .class .getSimpleName ()));
125+ }
126+ if (collection instanceof SortedSet <Value_ > || collection instanceof LinkedHashSet <Value_ >) {
127+ valueRange = new SetValueRange <>(set );
128+ } else {
129+ throw new IllegalStateException ("""
130+ The @%s-annotated member (%s) called on bean (%s) returns a Set (%s) with undefined iteration order.
131+ Maybe use SortedSet or LinkedHashSet to ensure solver reproducibility.
132+ """
133+ .formatted (ValueRangeProvider .class .getSimpleName (), memberAccessor , bean , set .getClass ()));
134+ }
135+ } else {
136+ List <Value_ > list = transformCollectionToList (collection );
137+ assertNullNotPresent (list , bean );
138+ valueRange = new ListValueRange <>(list );
139+ }
123140 } else {
124141 valueRange = (ValueRange <Value_ >) valueRangeObject ;
125142 }
@@ -133,6 +150,20 @@ protected <Value_> ValueRange<Value_> readValueRange(Object bean) {
133150 return valueRange ;
134151 }
135152
153+ private void assertNullNotPresent (List <?> list , Object bean ) {
154+ // Don't check the entire list for performance reasons, but do check common pitfalls
155+ if (!list .isEmpty () && (list .get (0 ) == null || list .get (list .size () - 1 ) == null )) {
156+ throw new IllegalStateException ("""
157+ The @%s-annotated member (%s) called on bean (%s) must not return a %s (%s) with an element that is null.
158+ Maybe remove that null element from the dataset.
159+ Maybe remove that null element from the dataset.
160+ Maybe use @%s(allowsUnassigned = true) or @%s(allowsUnassignedValues = true) instead."""
161+ .formatted (ValueRangeProvider .class .getSimpleName (), memberAccessor , bean ,
162+ collectionWrapping ? Collection .class .getSimpleName () : "array" , list ,
163+ PlanningVariable .class .getSimpleName (), PlanningListVariable .class .getSimpleName ()));
164+ }
165+ }
166+
136167 @ SuppressWarnings ("unchecked" )
137168 protected long readValueRangeSize (Object bean ) {
138169 var valueRangeObject = memberAccessor .executeGetter (bean );
@@ -162,27 +193,26 @@ protected long readValueRangeSize(Object bean) {
162193 }
163194 }
164195
165- private <T > List <T > transformCollectionToUniqueList (Collection <T > collection ) {
196+ private <T > List <T > transformCollectionToList (Collection <T > collection ) {
166197 if (collection .isEmpty ()) {
167198 return Collections .emptyList ();
199+ } else if (collection instanceof List <T > list ) {
200+ return list ;
201+ } else {
202+ return List .copyOf (collection );
168203 }
169- return collection .stream ()
170- .distinct ()
171- .toList ();
172204 }
173205
174206 @ SuppressWarnings ("unchecked" )
175- public static <Value_ > List <Value_ > transformArrayToUniqueList (Object arrayObject ) {
207+ public static <Value_ > List <Value_ > transformArrayToList (Object arrayObject ) {
176208 if (arrayObject == null ) {
177209 return Collections .emptyList ();
178210 }
179211 var array = (Value_ []) arrayObject ;
180212 if (array .length == 0 ) {
181213 return Collections .emptyList ();
182214 }
183- return Arrays .stream (array )
184- .distinct ()
185- .toList ();
215+ return Arrays .asList (array );
186216 }
187217
188218}
0 commit comments