1 ////////////////////////////////////////////////////////////////////////////////
2 // MillScript: an Open Spice interpreter and batch website creation tool
3 // Copyright (C) 2001-2004 Open World Ltd
4 //
5 // This file is part of MillScript.
6 //
7 // MillScript is free software; you can redistribute it and/or modify it under
8 // the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 2 of the License, or (at your option)
10 // any later version.
11 //
12 // MillScript is distributed in the hope that it will be useful, but WITHOUT
13 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 // more details.
16 //
17 // You should have received a copy of the GNU General Public License along with
18 // MillScript; if not, write to the Free Software Foundation, Inc., 59 Temple
19 // Place, Suite 330, Boston, MA 02111-1307 USA
20 ////////////////////////////////////////////////////////////////////////////////
21 package org.millscript.millscript.functions;
22
23 import org.millscript.commons.util.EList;
24 import org.millscript.commons.util.EMap;
25 import org.millscript.commons.util.IList;
26 import org.millscript.commons.util.IMap;
27 import org.millscript.commons.util.ListIterator;
28 import org.millscript.commons.util.MapIterator;
29 import org.millscript.commons.util.list.ELinkedList;
30 import org.millscript.commons.util.map.ELinkedHashMap;
31 import org.millscript.millscript.datatypes.MapAwareTools;
32 import org.millscript.millscript.vm.Machine;
33
34 /**
35 * This class implements the MillScript <code>groupPreserveOrder</code>
36 * function.
37 */
38 public final class GroupPreserveOrderFunction extends Function {
39
40 /**
41 * Returns the supplied data grouped by the specified field. The supplied
42 * data is sorted into a Map, whose keys are all the unique values for the
43 * specified field and whose values are the list of data which shares that
44 * unique value. This method is used for single level groupings, i.e.
45 * grouping by a single field.
46 *
47 * <p>
48 * In MillScript we might refer to this as a map-of-lists.
49 * </p>
50 *
51 * @param mc the current machine to perform operations on
52 * @param list a list of objects, with a map view, to group
53 * @param by the object/field to group by
54 * @return a map containing the grouped data, where the keys are the
55 * unique values for the specified field and the values a list
56 */
57 static EMap< Object, EList< IMap > > group( final Machine mc, final IList< IMap > list, final Object by ) {
58 // This map will hold the grouped data and is the map we will return
59 // NOTE - We use a LinkedHashMap here, so that the order we insert
60 // key-value pairs will be retained. i.e. the order of the
61 // original data is preserved.
62 EMap< Object, EList< IMap > > result = new ELinkedHashMap< Object, EList< IMap > >();
63 // We must iterate over all the items in the list of objects
64 ListIterator< IMap > it = list.iterator( true );
65 while ( it.hasNext() ) {
66 // Get the next object, we will refer to it as a record
67 final IMap record = it.nextValue();
68 // Get the value associated with the specified field for this
69 // record. We will refer to this as the key, as it's a key in the
70 // resulting map
71 final Object key = MapAwareTools.get( record, by );
72 // Check if the key exists in the grouped results
73 EList< IMap > sublist = result.get( key );
74 if ( sublist == null ) {
75 // The key doesn't exist, so we must create it
76 sublist = new ELinkedList< IMap >();
77 result.insert( key, sublist );
78 }
79 // Add the record to the end of the specified key
80 sublist.addLast( record );
81 }
82 // Return the grouped data
83 return result;
84 }
85
86 /**
87 * Returns the supplied data grouped by the specified fields. This is used
88 * where the group will have two or more levels. The list of objects that
89 * are to be grouped do not need to be maps, but they must have a map view.
90 *
91 * @param mc the current machine to perform operations on
92 * @param list a list of objects, with a map view, to group
93 * @param fields a list of fields to group the list by
94 * @param idx the index number of the field to group by, which is also
95 * effectively the current level in the group
96 * @param limit the number of levels this deep group will have
97 * @return a map containing the grouped data
98 */
99 @SuppressWarnings( "unchecked" )
100 static EMap deepGroup( final Machine mc, final IList< IMap > list, final IList< Object > fields, int idx, final int limit ) {
101 // Group the specified list of objects by the field for the current
102 // level of the grouping. e.g. on the first call to deepGroup, idx would
103 // be zero and we would group the specified list of objects by the first
104 // field in the list.
105 // NOTE - Maps with default values are required here so that we don't
106 // have to check to see if each level of a group exists when
107 // writing code.
108 EMap map = group( mc, list, fields.get( idx ) );
109 // Now we need to iterate through the all the values in this map and
110 // group the sublist of data recursively
111 MapIterator entries = map.iterator( true );
112 // Increment the current field counter, i.e. we are now one level
113 // further into the deep grouping
114 idx += 1;
115 // Have we reached the required depth?
116 if ( idx < limit ) {
117 // Set the default value for the map at the current level to be a
118 // special NullMapWithDefault, with the required depth.
119 map.setDefault( GroupFunction.makeDefault( limit - idx ) );
120 // Loop over all the keys in the map at this level of the group
121 while ( entries.hasNext() ) {
122 // Perform the next level of the deep group on the current
123 // sublist of data
124 Object key = entries.nextKey();
125 Object preNeed = entries.currentValue();
126 IList needsGrouping = (IList) preNeed;
127 map.update(
128 key,
129 deepGroup( mc, needsGrouping, fields, idx, limit )
130 );
131 }
132 }
133 return map;
134 }
135
136 /**
137 * @see org.millscript.millscript.functions.Function#apply(org.millscript.millscript.vm.Machine, int)
138 */
139 @Override
140 public void apply( final Machine mc, final int nargs ) {
141 if ( nargs <= 0 ) {
142 checkNargsGT( mc, 1, nargs );
143 } else if ( nargs == 2 ) {
144 Object by = mc.popObject();
145 IList list = mc.popIList();
146 mc.pushObject( group( mc, list, by ) );
147 } else if ( nargs >= 3 ) {
148 // This list will hold the fields to group by
149 EList< Object > fields = new ELinkedList< Object >();
150 // Pop each field name off the stack and add it to the front of the
151 // list. (arguments to a function are pushed onto the stack, hence
152 // you pop them off in reverse order)
153 for ( int i = 1; i < nargs; i++ ) {
154 fields.addFirst( mc.popObject() );
155 }
156 // Pop the ungrouped list of maps off the stack
157 IList list = mc.popIList();
158 // Push the grouped data onto the stack, i.e. from the script point
159 // of view, return the grouped data
160 mc.pushObject( deepGroup( mc, list, fields, 1, nargs ) );
161 }
162 /* otherwise nargs == 1 and we simply fall thru */
163 }
164 }