001    package ttsolver.constraint;
002    
003    import ifs.model.*;
004    import ttsolver.model.*;
005    import ifs.util.*;
006    import java.util.*;
007    import edu.purdue.smas.timetable.util.Constants;
008    
009    /**
010     * Departmental ballancing constraint.
011     * <br><br>
012     * The new implementation of the balancing times for departments works as follows: Initially, there is a histogram for 
013     * each department computed. For each time slot, it says how many placements of all classes (of the department) include 
014     * this time slot. Each such placement has the weight of 1 / number of placements of the class, so the total sum of all 
015     * values in the histogram (i.e., for all time slots) is equal to the total sum of half-hours required by the given set 
016     * of classes.
017     * <br><br>
018     * On the other hand, each class splits its number of half-hours (e.g., 2x50 has 4 half-hours, "4 points") into the 
019     * time slots which it can occupy, according to the frequencies of the utilization of each time slots (i.e., number of 
020     * placements containing the time slots divided by the number of all placements of the class).
021     * <br><br>
022     * For example, a histogram for department 1286:<code><br>
023     * 1: [0.10,0.00,0.10,0.00,0.10] <- 7:30 [Mon, Tue, Wed, Thu, Fri]<br>
024     * 2: [0.10,0.00,0.10,0.00,0.10] <- 8:00 [Mon, Tue, Wed, Thu, Fri]<br>
025     * 3: [0.35,0.62,0.48,0.62,0.10] ... and so on<br>
026     * 4: [0.35,0.62,0.48,0.62,0.10]<br>
027     * 5: [1.35,1.12,1.48,0.12,1.10]<br>
028     * 6: [1.35,1.12,1.48,0.12,1.10]<br>
029     * 7: [0.35,0.62,0.48,1.63,0.10]<br>
030     * 8: [0.35,0.62,0.48,1.63,0.10]<br>
031     * 9: [0.35,0.12,0.48,0.12,0.10]<br>
032     * 10:[0.35,0.12,0.48,0.12,0.10]<br>
033     * 11:[0.35,0.12,0.48,0.12,0.10]<br>
034     * 12:[0.35,0.12,0.48,0.12,0.10]<br>
035     * 13:[0.35,0.12,0.48,1.12,0.10]<br>
036     * 14:[0.35,0.12,0.48,1.12,0.10]<br>
037     * 15:[0.35,0.12,0.48,0.12,0.10]<br>
038     * 16:[0.35,0.12,0.48,0.12,0.10]<br>
039     * 17:[0.35,0.12,0.48,0.12,0.10]<br>
040     * 18:[0.35,0.12,0.48,0.12,0.10]<br>
041     * 19:[0.10,0.00,0.10,0.00,0.10]<br>
042     * 20:[0.10,0.00,0.10,0.00,0.10]<br>
043     * 21:[0.00,0.00,0.00,0.00,0.00]<br>
044     * </code><br>
045     * You can easily see, that the time slots which are prohibited for all of the classes of the department have zero 
046     * values, also some time slots are used much often than the others. Note that there are no preferences involved in 
047     * this computation, only prohibited / not used times are less involved.
048     * <br><br>
049     * The idea is to setup the initial limits for each of the time slots according to the above values. The reason for doing 
050     * so is to take the requirements (time patterns, required/prohibited times) of all classes of the department into 
051     * account. For instance, take two classes A and B of type MWF 2x100 with two available locations starting from 7:30 
052     * and 8:30. Note that the time slot Monday 8:30-9:00 will be always used by both of the classes and for instance the 
053     * time slot Monday 7:30-8:00 (or Friday 9:30-10:00) can be used by none of them, only one of them or both of them. 
054     * From the balancing point of the view, I believe it should be preferred to have one class starting from 7:30 and the 
055     * other one from 8:30.
056     * <br><br>
057     * So, after the histogram is computed, its values are increased by the given percentage (same reason as before, to 
058     * allow some space between the actual value and the limit, also not to make the limits for a department with 21 time 
059     * slots less strict than a department with 20 time slots etc.). The initial limits are than computed as these values 
060     * rounded upwards (1.90 results in 2).
061     * <br><br>
062     * Moreover, the value is increased only when the histogram value of a time slot is below the following value: 
063     * spread factor * (number of used time slots / number of all time slots). Is assures a department of at least the 
064     * given percentage more time slots than required, but does not provide an additional reward for above average 
065     * use of time slots based on 'required' times.
066     * <br><br>
067     * For example, the department 1286 will have the following limits (histogram increased by 20% (i.e., each value is 20% 
068     * higher) and rounded upwards):
069     * <code><br>
070     * 1: [1,0,1,0,1]<br>
071     * 2: [1,0,1,0,1]<br>
072     * 3: [1,1,1,1,1]<br>
073     * 4: [1,1,1,1,1]<br>
074     * 5: [2,2,2,1,2]<br>
075     * 6: [2,2,2,1,2]<br>
076     * 7: [1,1,1,2,1]<br>
077     * 8: [1,1,1,2,1]<br>
078     * 9: [1,1,1,1,1]<br>
079     * 10:[1,1,1,1,1]<br>
080     * 11:[1,1,1,1,1]<br>
081     * 12:[1,1,1,1,1]<br>
082     * 13:[1,1,1,2,1]<br>
083     * 14:[1,1,1,2,1]<br>
084     * 15:[1,1,1,1,1]<br>
085     * 16:[1,1,1,1,1]<br>
086     * 17:[1,1,1,1,1]<br>
087     * 18:[1,1,1,1,1]<br>
088     * 19:[1,0,1,0,1]<br>
089     * 20:[1,0,1,0,1]<br>
090     * 21:[0,0,0,0,0]<br>
091     * </code><br>
092     * The maximal penalty (i.e., the maximal number of half-hours which can be used above the pre-computed limits by a 
093     * department) of a constraint is used. Initially, the maximal penalty is set to zero. It is increased by one after 
094     * each time when the constraint causes the given number (e.g., 100) of un-assignments.
095     * <br><br>
096     * Also, the balancing penalty (the total number of half-hours over the initial limits) is computed and it can be 
097     * minimized during the search (soft constraint).
098     * <br><br>
099     * Parameters:
100     * <table border='1'><tr><th>Parameter</th><th>Type</th><th>Comment</th></tr>
101     * <tr><td>DepartmentSpread.SpreadFactor</td><td>{@link Double}</td><td>Initial allowance of the slots for a particular time (factor)<br>Allowed slots = ROUND(SpreadFactor * (number of requested slots / number of slots per day))</td></tr>
102     * <tr><td>DepartmentSpread.Unassignments2Weaken</td><td>{@link Integer}</td><td>Increase the initial allowance when it causes the given number of unassignments</td></tr>
103     * </table>
104     *
105     * @author <a href="mailto:muller@ktiml.mff.cuni.cz">Tomáš Müller</a>
106     * @version 1.0
107     */
108    
109    public class DepartmentSpreadConstraint extends Constraint {
110        private static org.apache.log4j.Logger sLogger = org.apache.log4j.Logger.getLogger(DepartmentSpreadConstraint.class);
111        private static java.text.DecimalFormat sDoubleFormat = new java.text.DecimalFormat("0.00",new java.text.DecimalFormatSymbols(Locale.US));
112        private static java.text.DecimalFormat sIntFormat = new java.text.DecimalFormat("0",new java.text.DecimalFormatSymbols(Locale.US));
113        private int iMaxCourses[][] = null;
114        private int iCurrentPenalty = 0;
115        private int iMaxAllowedPenalty = 0;
116        
117        private String iDepartment = null;
118        private boolean iInitialized = false;
119        private int[][] iNrCourses = null;
120        private Vector [][] iCourses = null;
121        private double iSpreadFactor = 1.20;
122        private int iUnassignmentsToWeaken = 250;
123        private long iUnassignment = 0;
124        
125        public DepartmentSpreadConstraint(DataProperties config, String department) {
126            iDepartment = department;
127            iSpreadFactor = config.getPropertyDouble("DepartmentSpread.SpreadFactor",iSpreadFactor);
128            iUnassignmentsToWeaken = config.getPropertyInt("DepartmentSpread.Unassignments2Weaken", iUnassignmentsToWeaken);
129            iNrCourses = new int[Constants.SLOTS_PER_DAY_NO_EVENINGS][Constants.DAY_CODES.length];
130            iCourses = new Vector[Constants.SLOTS_PER_DAY_NO_EVENINGS][Constants.DAY_CODES.length];
131            for (int i=0;i<iNrCourses.length;i++) {
132                for (int j=0;j<Constants.DAY_CODES.length;j++) {
133                    iNrCourses[i][j] = 0;
134                    iCourses[i][j]=new Vector(10,5);
135                }
136            }
137        }
138        
139        /** Initialize constraint (to be called after all variables are added to this constraint) */
140        public void init() {
141            if (iInitialized) return;
142            double histogramPerDay[][] = new double[Constants.SLOTS_PER_DAY_NO_EVENINGS][Constants.DAY_CODES.length];
143            iMaxCourses = new int[Constants.SLOTS_PER_DAY_NO_EVENINGS][Constants.DAY_CODES.length];
144            for (int i=0;i<Constants.SLOTS_PER_DAY_NO_EVENINGS;i++)
145                for (int j=0;j<Constants.DAY_CODES.length;j++)
146                    histogramPerDay[i][j]=0.0;
147            int totalUsedSlots = 0;
148            for (Enumeration e=variables().elements();e.hasMoreElements();) {
149                Lecture lecture = (Lecture)e.nextElement();
150                Placement firstPlacement = (Placement)lecture.values().firstElement();
151                if (firstPlacement!=null) {
152                    totalUsedSlots += firstPlacement.getTimeLocation().getNrHalfHoursPerMeeting()*firstPlacement.getTimeLocation().getNrMeetings();
153                }
154                for (Enumeration e2=lecture.values().elements();e2.hasMoreElements();) {
155                    Placement p = (Placement)e2.nextElement();
156                    int firstSlot = p.getTimeLocation().getStartSlot();
157                    if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) continue;
158                    int endSlot = firstSlot+p.getTimeLocation().getNrHalfHoursPerMeeting()-1;
159                    for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
160                        int dayCode = p.getTimeLocation().getDayCode();
161                        for (int j=0;j<Constants.DAY_CODES.length;j++) {
162                            if ((dayCode & Constants.DAY_CODES[j])!=0) {
163                                histogramPerDay[i][j] += 1.0 / lecture.values().size();
164                            }
165                        }
166                    }
167                }
168            }
169            //System.out.println("Histogram for department "+iDepartment+":");
170            double threshold = iSpreadFactor*((double)totalUsedSlots/(5.0*Constants.SLOTS_PER_DAY_NO_EVENINGS));
171            //System.out.println("Threshold["+iDepartment+"] = "+threshold);
172            int totalAvailableSlots = 0;
173            for (int i=0;i<Constants.SLOTS_PER_DAY_NO_EVENINGS;i++) {
174                //System.out.println("  "+fmt(i+1)+": "+fmt(histogramPerDay[i]));
175                for (int j=0;j<Constants.DAY_CODES.length;j++) {
176                    iMaxCourses[i][j]=(int)(0.999+(histogramPerDay[i][j]<=threshold?iSpreadFactor*histogramPerDay[i][j]:histogramPerDay[i][j]));
177                    totalAvailableSlots += iMaxCourses[i][j];
178                }
179            }
180            iCurrentPenalty = 0;
181            for (Enumeration e=variables().elements();e.hasMoreElements();) {
182                Lecture lecture = (Lecture)e.nextElement();
183                Placement placement = (Placement)lecture.getAssignment();
184                if (placement==null) continue;
185                int firstSlot = placement.getTimeLocation().getStartSlot();
186                if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) continue;
187                int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
188                for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
189                    for (int j=0;j<Constants.DAY_CODES.length;j++) {
190                        int dayCode = Constants.DAY_CODES[j];
191                        if ((dayCode & placement.getTimeLocation().getDayCode()) != 0) {
192                            iNrCourses[i][j]++;
193                            iCourses[i][j].addElement(placement);
194                            iCurrentPenalty += Math.max(0,iNrCourses[i][j]-iMaxCourses[i][j]);
195                        }
196                    }
197                }
198            }
199            iMaxAllowedPenalty = iCurrentPenalty;
200            //System.out.println("Initial penalty = "+fmt(iMaxAllowedPenalty));
201            iInitialized = true;
202        }
203        
204        private Lecture getAdept(Placement placement, int[][] nrCourses, Set conflicts) {
205            //sLogger.debug("  -- looking for an adept");
206            Lecture adept = null;
207            int improvement = 0;
208            for (Enumeration e=variables().elements();e.hasMoreElements();) {
209                Lecture lect = (Lecture)e.nextElement();
210                if (lect.getAssignment()==null || lect.equals(placement.variable())) continue;
211                if (conflicts.contains(lect.getAssignment())) continue;
212                int imp = getPenaltyIfUnassigned((Placement)lect.getAssignment(),nrCourses);
213                if (imp==0) continue;
214                //sLogger.debug("    -- "+lect+" can decrease penalty by "+imp);
215                if (adept==null || imp>improvement) {
216                    adept = lect;
217                    improvement = imp;
218                }
219            }
220            //sLogger.debug("  -- adept "+adept+" selected, penalty will be decreased by "+improvement);
221            return adept;
222        }
223        
224        private int getPenaltyIfUnassigned(Placement placement, int[][] nrCourses)  {
225            int firstSlot = placement.getTimeLocation().getStartSlot();
226            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return 0;
227            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
228            int penalty = 0;
229            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
230                for (int j=0;j<Constants.DAY_CODES.length;j++) {
231                    int dayCode = Constants.DAY_CODES[j];
232                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0 &&
233                        nrCourses[i][j]>iMaxCourses[i][j]) penalty ++;
234                }
235            }
236            return penalty;
237        }
238    
239        private int tryUnassign(Placement placement, int[][] nrCourses) {
240            //sLogger.debug("  -- trying to unassign "+placement);
241            int firstSlot = placement.getTimeLocation().getStartSlot();
242            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return 0;
243            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
244            int improvement = 0;
245            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
246                for (int j=0;j<Constants.DAY_CODES.length;j++) {
247                    int dayCode = Constants.DAY_CODES[j];
248                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0) {
249                        if (nrCourses[i][j]>iMaxCourses[i][j]) improvement++;
250                        nrCourses[i][j]--;
251                    }
252                }
253            }
254            //sLogger.debug("  -- penalty is decreased by "+improvement);
255            return improvement;
256        }
257    
258        private int tryAssign(Placement placement, int[][] nrCourses) {
259            //sLogger.debug("  -- trying to assign "+placement);
260            int firstSlot = placement.getTimeLocation().getStartSlot();
261            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return 0;
262            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
263            int penalty = 0;
264            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
265                for (int j=0;j<Constants.DAY_CODES.length;j++) {
266                    int dayCode = Constants.DAY_CODES[j];
267                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0) {
268                        nrCourses[i][j]++;
269                        if (nrCourses[i][j]>iMaxCourses[i][j]) penalty++;
270                    }
271                }
272            }
273            //sLogger.debug("  -- penalty is incremented by "+penalty);
274            return penalty;
275        }
276    
277        public void computeConflicts(Value value, Set conflicts) {
278            if (!iInitialized || iUnassignmentsToWeaken==0) return;
279            Placement placement = (Placement)value;
280            int penalty = iCurrentPenalty + getPenalty(placement);
281            if (penalty<=iMaxAllowedPenalty) return;
282            int firstSlot = placement.getTimeLocation().getStartSlot();
283            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return;
284            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
285                //sLogger.debug("-- computing conflict for value "+value+" ... (penalty="+iCurrentPenalty+", penalty with the value="+penalty+", max="+iMaxAllowedPenalty+")");
286                int[][] nrCourses = new int[iNrCourses.length][Constants.DAY_CODES.length];
287                for (int i=0;i<iNrCourses.length;i++)
288                    for (int j=0;j<Constants.DAY_CODES.length;j++)
289                        nrCourses[i][j] = iNrCourses[i][j];
290                tryAssign(placement, nrCourses);
291                //sLogger.debug("  -- nrCurses="+fmt(nrCourses));
292                for (Enumeration e=variables().elements();e.hasMoreElements();) {
293                    Lecture lect = (Lecture)e.nextElement();
294                    if (conflicts.contains(lect)) {
295                        penalty -= tryUnassign((Placement)lect.getAssignment(), nrCourses);
296                    }
297                    if (penalty<=iMaxAllowedPenalty) return;
298                }
299                while (penalty>iMaxAllowedPenalty) {
300                    Lecture lect = getAdept(placement, nrCourses, conflicts);
301                    if (lect==null) break;
302                    conflicts.add(lect.getAssignment());
303                    //sLogger.debug("  -- conflict "+lect.getAssignment()+" added");
304                    penalty -= tryUnassign((Placement)lect.getAssignment(), nrCourses);
305                }
306        }
307        
308        public boolean inConflict(Value value) {
309            if (!iInitialized || iUnassignmentsToWeaken==0) return false;
310            Placement placement = (Placement)value;
311                return getPenalty(placement)+iCurrentPenalty>iMaxAllowedPenalty;
312        }
313        
314        public boolean isConsistent(Value value1, Value value2) {
315            if (!iInitialized || iUnassignmentsToWeaken==0) return true;
316            Placement p1 = (Placement)value1;
317            Placement p2 = (Placement)value2;
318            if (!p1.getTimeLocation().hasIntersection(p2.getTimeLocation())) return true;
319            int firstSlot = Math.max(p1.getTimeLocation().getStartSlot(),p2.getTimeLocation().getStartSlot());
320            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return true;
321            int endSlot = Math.min(p1.getTimeLocation().getStartSlot()+p1.getTimeLocation().getNrHalfHoursPerMeeting()-1,p2.getTimeLocation().getStartSlot()+p2.getTimeLocation().getNrHalfHoursPerMeeting()-1);
322                int penalty = 0;
323                for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
324                    for (int j=0;j<Constants.DAY_CODES.length;j++) {
325                        int dayCode = Constants.DAY_CODES[j];
326                        if ((dayCode & p1.getTimeLocation().getDayCode()) != 0 && (dayCode & p2.getTimeLocation().getDayCode()) != 0) {
327                            penalty += Math.max(0,2-iMaxCourses[i][j]);
328                        }
329                    }
330                }
331                return (penalty<iMaxAllowedPenalty);
332        }
333        
334        private void weaken() {
335            if (!iInitialized) return;
336            if (iUnassignmentsToWeaken==0) return;
337                iMaxAllowedPenalty ++;
338        }
339        
340        public void assigned(long iteration, Value value) {
341            super.assigned(iteration, value);
342            if (!iInitialized) return;
343            Placement placement = (Placement)value;
344            int firstSlot = placement.getTimeLocation().getStartSlot();
345            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return;
346            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
347            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
348                for (int j=0;j<Constants.DAY_CODES.length;j++) {
349                    int dayCode = Constants.DAY_CODES[j];
350                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0) {
351                        iNrCourses[i][j]++;
352                        if (iNrCourses[i][j]>iMaxCourses[i][j]) iCurrentPenalty++;
353                        iCourses[i][j].addElement(value);
354                    }
355                }
356            }
357        }
358        
359        public void unassigned(long iteration, Value value) {
360            super.unassigned(iteration, value);
361            if (!iInitialized) return;
362            Placement placement = (Placement)value;
363            int firstSlot = placement.getTimeLocation().getStartSlot();
364            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return;
365            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
366            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
367                for (int j=0;j<Constants.DAY_CODES.length;j++) {
368                    int dayCode = Constants.DAY_CODES[j];
369                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0) {
370                        if (iNrCourses[i][j]>iMaxCourses[i][j]) iCurrentPenalty--;
371                        iNrCourses[i][j]--;
372                        iCourses[i][j].removeElement(value);
373                    }
374                }
375            }
376        }
377        
378        /** Increment unassignment counter */
379        public void incUnassignmentCounter(Value value) {
380            iUnassignment ++;
381            if (iUnassignment%iUnassignmentsToWeaken==0)
382                weaken();
383        }
384        
385        public String toString() {
386            if (!iInitialized) return iDepartment+" (not initialized)";
387            return iDepartment+" (p="+fmt(getPenalty())+", mx="+fmt(iMaxCourses)+", mp="+fmt(iMaxAllowedPenalty)+"): "+fmt(iNrCourses);
388        }
389    
390        /** Department balancing penalty for this department */
391        public int getPenalty() {
392            if (!iInitialized) return 0;
393            return iCurrentPenalty;
394        }
395        
396        /** Department balancing penalty of the given placement */
397        public int getPenalty(Placement placement) {
398            if (!iInitialized) return 0;
399            int firstSlot = placement.getTimeLocation().getStartSlot();
400            if (firstSlot>=Constants.SLOTS_PER_DAY_NO_EVENINGS) return 0;
401            int endSlot = firstSlot+placement.getTimeLocation().getNrHalfHoursPerMeeting()-1;
402            int penalty = 0;
403            for (int i=firstSlot;i<=Math.min(endSlot,Constants.SLOTS_PER_DAY_NO_EVENINGS-1);i++) {
404                for (int j=0;j<Constants.DAY_CODES.length;j++) {
405                    int dayCode = Constants.DAY_CODES[j];
406                    if ((dayCode & placement.getTimeLocation().getDayCode()) != 0 &&
407                        iNrCourses[i][j]>=iMaxCourses[i][j]) penalty ++;
408                }
409            }
410            return penalty;
411        }
412            
413        private String fmt(double value) {
414            return sDoubleFormat.format(value);
415        }
416        
417        private String fmt(double[] values) {
418            if (values==null) return null;
419            StringBuffer sb = new StringBuffer("[");
420            for (int i=0;i<Math.min(5,values.length);i++)
421                sb.append(i==0?"":",").append(sDoubleFormat.format(values[i]));
422                sb.append("]");
423                return sb.toString();
424        }
425        
426        private String fmt(double[][] values) {
427            if (values==null) return null;
428            StringBuffer sb = new StringBuffer("[");
429            for (int i=0;i<values.length;i++) {
430                /*double total = 0;
431                for (int j=0;j<values[i].length;j++)
432                    total += values[i][j];
433                sb.append(i==0?"":",").append(fmt(total));*/
434                sb.append(i==0?"":",").append(fmt(values[i]));
435            }
436            sb.append("]");
437            return sb.toString();
438        }
439        
440        private String fmt(int value) {
441            return sIntFormat.format(value);
442        }
443        
444        private String fmt(int[] values) {
445            if (values==null) return null;
446            StringBuffer sb = new StringBuffer("[");
447            for (int i=0;i<Math.min(5,values.length);i++)
448                sb.append(i==0?"":",").append(sIntFormat.format(values[i]));
449                sb.append("]");
450                return sb.toString();
451        }
452        
453        private String fmt(int[][] values) {
454            if (values==null) return null;
455            StringBuffer sb = new StringBuffer("[");
456            for (int i=0;i<values.length;i++) {
457                /*int total = 0;
458                for (int j=0;j<values[i].length;j++)
459                    total += values[i][j];
460                sb.append(i==0?"":",").append(fmt(total));*/
461                sb.append(i==0?"":",").append(fmt(values[i]));
462            }
463            sb.append("]");
464            return sb.toString();
465        }
466    }