View Javadoc

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 }