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.IMapDefault;
28  import org.millscript.commons.util.ListIterator;
29  import org.millscript.commons.util.MapIterator;
30  import org.millscript.commons.util.defaults.ConstantMapDefault;
31  import org.millscript.commons.util.list.ELinkedList;
32  import org.millscript.commons.util.map.EBinaryTreeMap;
33  import org.millscript.commons.util.map.IEmptyMap;
34  import org.millscript.millscript.datatypes.MapAwareTools;
35  import org.millscript.millscript.vm.Machine;
36  
37  /**
38   * This class implements the MillScript <code>group</code> function.
39   */
40  public final class GroupFunction extends Function {
41  
42      /**
43       * Returns an empty map with a chain of empty maps as their default values,
44       * up to the required depth. The last map in the chain has a default value
45       * of <code>null</code>.
46       * <p>
47       * For example, if the depth is one, this will return an empty map with a
48       * default value of <code>null</code>. If the depth is two, this will
49       * return an empty map with a default value of an empty map with a default
50       * value of <code>null</code>, etc.
51       * </p>
52       *
53       * @param depth the required depth of map
54       * @return  an empty map with a chain of empty maps as their default values
55       * of the specified depth
56       */
57      static IMapDefault makeDefault( final int depth ) {
58          IMap< Object, Object > nullMap = new IEmptyMap< Object, Object >();
59          for ( int i = 1; i < depth; i++ ) {
60              final IMapDefault< Object, Object > mapDefault = new ConstantMapDefault< Object, Object >( nullMap );
61              nullMap = new IEmptyMap< Object, Object >();
62              nullMap.setDefault( mapDefault );
63          }
64          return new ConstantMapDefault< Object, Object >( nullMap );
65      }
66  
67      /**
68       * Returns the supplied data grouped by the specified field. The supplied
69       * data is sorted into a Map, whose keys are all the unique values for the
70       * specified field and whose values are the list of data which shares that
71       * unique value. This method is used for single level groupings, i.e.
72       * grouping by a single field.
73       *
74       * <p>
75       * In MillScript we might refer to this as a map-of-lists.
76       * </p>
77       *
78       * @param   mc      the current machine to perform operations on
79       * @param   list    a list of objects, with a map view, to group
80       * @param   by      the object/field to group by
81       * @return  a map containing the grouped data, where the keys are the
82       *          unique values for the specified field and the values a list
83       */
84      @SuppressWarnings( "unchecked" )
85      static EMap group( final Machine mc, final IList list, final Object by ) {
86          // This map will hold the grouped data and is the map we will return
87          EMap result = new EBinaryTreeMap();
88          // We must iterate over all the items in the list of objects
89          ListIterator< ? > it = list.iterator( true );
90          while ( it.hasNext() ) {
91              // Get the next object, we will refer to it as a record
92              final Object record = it.nextValue();
93              // Get the value associated with the specified field for this
94              // record. We will refer to this as the key, as it's a key in the
95              // resulting map
96              final Object key = MapAwareTools.get( record, by );
97              // Check if the key exists in the grouped results
98              EList sublist = (EList)result.get( key );
99              if ( sublist == null ) {
100                 // The key doesn't exist, so we must create it
101                 sublist = new ELinkedList();
102                 result.insert( key, sublist );
103             }
104             // Add the record to the end of the specified key
105             sublist.addLast( record );
106         }
107         // Return the grouped data
108         return result;
109     }
110 
111     /**
112      * Returns the supplied data grouped by the specified fields. This is used
113      * where the group will have two or more levels. The list of objects that
114      * are to be grouped do not need to be maps, but they must have a map view.
115      *
116      * @param   mc      the current machine to perform operations on
117      * @param   list    a list of objects, with a map view, to group
118      * @param   fields  a list of fields to group the list by
119      * @param   idx     the index number of the field to group by, which is also
120      *                  effectively the current level in the group
121      * @param   limit   the number of levels this deep group will have
122      * @return  a map containing the grouped data
123      */
124     @SuppressWarnings( "unchecked" )
125     static IMap deepGroup( final Machine mc, final IList list, final IList fields, int idx, final int limit ) {
126         // Group the specified list of objects by the field for the current
127         // level of the grouping. e.g. on the first call to deepGroup, idx would
128         // be zero and we would group the specified list of objects by the first
129         // field in the list.
130         // NOTE - This is a good indication of the usefullness of maps with
131         //        default values, as it allows us to skip checks to see if each
132         //        level of a group exists when writing MillScript code.
133         EMap map = group( mc, list, fields.get( idx ) );
134         // Now we need to iterate through the all the values in this map and
135         // group the sublist of data recursively
136         MapIterator entries = map.iterator( true );
137         // Increment the current field counter, i.e. we are now one level
138         // further into the deep grouping
139         idx += 1;
140         // Have we reached the required depth?
141         if ( idx < limit ) {
142             // Set the default value for the map at the current level to be a
143             // special NullMapWithDefault, with the required depth.
144             map.setDefault( GroupFunction.makeDefault( limit - idx ) );
145             // Loop over all the keys in the map at this level of the group
146             while ( entries.hasNext() ) {
147                 // Perform the next level of the deep group on the current
148                 // sublist of data
149                 map.insert(
150                     entries.nextKey(),
151                     deepGroup( mc, (IList) entries.currentValue(), fields, idx, limit )
152                 );
153             }
154         }
155         return map;
156     }
157 
158     /**
159      * @see org.millscript.millscript.functions.Function#apply(org.millscript.millscript.vm.Machine, int)
160      */
161     @Override
162     public void apply( final Machine mc, final int nargs ) {
163         if ( nargs <= 0 ) {
164             checkNargsGT( mc, 1, nargs );
165         } else if ( nargs == 2 ) {
166             Object by = mc.popObject();
167             IList list = mc.popIList();
168             mc.pushObject( group( mc, list, by ) );
169         } else if ( nargs >= 3 ) {
170             // This list will hold the fields to group by
171             EList< Object > fields = new ELinkedList< Object >();
172             // Pop each field name off the stack and add it to the front of the
173             // list. (arguments to a function are pushed onto the stack, hence
174             // you pop them off in reverse order)
175             for ( int i = 1; i < nargs; i++ ) {
176                 fields.addFirst( mc.popObject() );
177             }
178             // Pop the ungrouped list of maps off the stack
179             IList list = mc.popIList();
180             // Push the grouped data onto the stack, i.e. from the script point
181             // of  view, return the grouped data
182             mc.pushObject( deepGroup( mc, list, fields, 1, nargs ) );
183         }
184         /* otherwise nargs == 1 and we simply fall thru */
185     }
186 
187 }