001package com.hfg.util.collection;
002
003
004import java.io.IOException;
005import java.io.Writer;
006import java.util.*;
007
008import com.hfg.exception.ProgrammingException;
009
010//------------------------------------------------------------------------------
011/**
012 A symmetric matrix (a square matrix with values that are symmetric with respect to the diagonal).
013 <div>
014 Ex:<pre>
015  1   5  -3
016  5   2   9
017 -3   9   8
018 </pre>
019 </div>
020 <div>
021 See <a href='http://en.wikipedia.org/wiki/Symmetric_matrix'>http://en.wikipedia.org/wiki/Symmetric_matrix</a>
022 </div>
023 * @author J. Alex Taylor, hairyfatguy.com
024 */
025//------------------------------------------------------------------------------
026// com.hfg Library
027//
028// This library is free software; you can redistribute it and/or
029// modify it under the terms of the GNU Lesser General Public
030// License as published by the Free Software Foundation; either
031// version 2.1 of the License, or (at your option) any later version.
032//
033// This library is distributed in the hope that it will be useful,
034// but WITHOUT ANY WARRANTY; without even the implied warranty of
035// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
036// Lesser General Public License for more details.
037//
038// You should have received a copy of the GNU Lesser General Public
039// License along with this library; if not, write to the Free Software
040// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
041//
042// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
043// jataylor@hairyfatguy.com
044//------------------------------------------------------------------------------
045
046public class SymmetricMatrix<K extends Comparable, V extends Comparable> implements Cloneable
047{
048   protected List<MatrixRow> mMatrix;
049
050   private HashMap<K, MatrixRow> mKeyMap;
051
052   // Cached values
053   private Integer mSize;
054   private SortedList<MatrixCell<K, K, V>> mSmallestValues;
055   private boolean mSmallestValuesIncludeIdentity;
056
057   private static int sDefaultInitialCapacity = 99;
058   private static int sSmallestValuesCapacity = 1000;
059   private static int sSmallestValuesRetentionSize = 750;
060
061
062   //##########################################################################
063   // CONSTRUCTORS
064   //##########################################################################
065
066   //--------------------------------------------------------------------------
067   public SymmetricMatrix()
068   {
069      this(sDefaultInitialCapacity);
070   }
071
072   //--------------------------------------------------------------------------
073   public SymmetricMatrix(int inInitialCapacity)
074   {
075      mKeyMap = new HashMap<>(inInitialCapacity);
076      mMatrix = new ArrayList<>(inInitialCapacity);
077   }
078
079   //##########################################################################
080   // PUBLIC METHODS
081   //##########################################################################
082
083   //--------------------------------------------------------------------------
084   public void put(K inKey1, K inKey2, V inValue)
085   {
086      MatrixRow matrixRow1 = mKeyMap.get(inKey1);
087      if (null == matrixRow1)
088      {
089         matrixRow1 = innerAddKey(inKey1);
090      }
091
092      MatrixRow matrixRow2 = mKeyMap.get(inKey2);
093      if (null == matrixRow2)
094      {
095         matrixRow2 = innerAddKey(inKey2);
096      }
097
098
099      MatrixRow rowWithCell = (matrixRow1.getIndex() < matrixRow2.getIndex() ? matrixRow2 : matrixRow1);
100      rowWithCell.getData().set((matrixRow1 == rowWithCell ? matrixRow2 : matrixRow1).getIndex(), inValue);
101
102      // If there is a smallest value cache, see if the new value should be added
103      if (mSmallestValues != null
104          && inValue.compareTo(mSmallestValues.last().getValue()) <= 0
105          && (mSmallestValuesIncludeIdentity || inKey1 != inKey2))
106      {
107         mSmallestValues.add(new MatrixCell<>(inKey1, inKey2, inValue));
108         if (mSmallestValues.size() > sSmallestValuesCapacity - 2)
109         {
110            // Truncate the list
111            mSmallestValues.truncate(sSmallestValuesRetentionSize);
112         }
113      }
114   }
115
116   //--------------------------------------------------------------------------
117   public V get(K inKey1, K inKey2)
118   {
119      MatrixRow matrixRow1 = mKeyMap.get(inKey1);
120      if (null == matrixRow1)
121      {
122         throw new RuntimeException("'" + inKey1 + "' is not a valid matrix key!");
123      }
124
125      MatrixRow matrixRow2 = mKeyMap.get(inKey2);
126      if (null == matrixRow2)
127      {
128         throw new RuntimeException("'" + inKey2 + "' is not a valid matrix key!");
129      }
130
131      MatrixRow rowWithCell = (matrixRow1.getIndex() < matrixRow2.getIndex() ? matrixRow2 : matrixRow1);
132      V value = rowWithCell.getData().get((matrixRow1 == rowWithCell ? matrixRow2 : matrixRow1).getIndex());
133
134      return value;
135   }
136
137   //--------------------------------------------------------------------------
138   // TODO:
139   public void toString(Writer inWriter)
140         throws IOException
141   {
142      int maxKeyLength = getMaxKeyLength();
143      String keyFormat = "%-" + maxKeyLength + "." + maxKeyLength + "s";
144
145      Set<K> orderedKeys = orderedKeySet();
146      for (K key1 : orderedKeys)
147      {
148         inWriter.append(String.format(keyFormat, key1));
149         for (K key2 : orderedKeys)
150         {
151            inWriter.append(String.format("  %.4f", get(key1, key2)));
152
153            if (key1 == key2)
154            {
155               break;
156            }
157         }
158
159         inWriter.append(System.getProperty("line.separator"));
160      }
161   }
162
163   //--------------------------------------------------------------------------
164   /**
165    * Returns the keys in no guaranteed order.
166    * @return a Set containing the matrix keys
167    */
168   public Set<K> keySet()
169   {
170      return mKeyMap.keySet();
171   }
172
173   //--------------------------------------------------------------------------
174   /**
175    * Returns the keys in addition order.
176    * @return a Set containing the matrix keys in the order of addition
177    */
178   public Set<K> orderedKeySet()
179   {
180      OrderedSet<K> keySet = null;
181
182      if (CollectionUtil.hasValues(mMatrix))
183      {
184         keySet = new OrderedSet<>(mMatrix.size());
185         for (MatrixRow matrixRow : mMatrix)
186         {
187            keySet.add(matrixRow.getKey());
188         }
189      }
190
191      return keySet;
192   }
193
194   //--------------------------------------------------------------------------
195   public int numKeys()
196   {
197      return (mKeyMap != null ? mKeyMap.size() : 0);
198   }
199
200   //--------------------------------------------------------------------------
201   public boolean addKey(K inKey)
202   {
203      boolean added = false;
204      if (! mKeyMap.containsKey(inKey))
205      {
206         innerAddKey(inKey);
207
208         added = true;
209      }
210
211      return added;
212   }
213
214   //--------------------------------------------------------------------------
215   public boolean containsKey(K inKey)
216   {
217      return mKeyMap.containsKey(inKey);
218   }
219
220   //--------------------------------------------------------------------------
221   // TODO: Should this return Map<K, V> ?
222   public void removeKey(K inKey)
223   {
224      MatrixRow matrixRow = mKeyMap.get(inKey);
225      if (matrixRow != null)
226      {
227         int removedIndex = matrixRow.getIndex();
228
229         mKeyMap.remove(inKey);
230         mMatrix.remove(removedIndex);
231         for (int i = removedIndex; i < mMatrix.size(); i++)
232         {
233            MatrixRow matrixRowToAdjust = mMatrix.get(i);
234            matrixRowToAdjust.decrementIndex();
235            matrixRowToAdjust.getData().remove(removedIndex);
236         }
237
238         // If any of the cached smallest values are associated with the removed key, remove them
239         if (mSmallestValues != null)
240         {
241            for (int i = 0; i < mSmallestValues.size(); i++)
242            {
243               MatrixCell<K, K, V> matrixCell = mSmallestValues.get(i);
244               if (matrixCell.getRowKey().equals(inKey)
245                   || matrixCell.getColKey().equals(inKey))
246               {
247                  mSmallestValues.remove(i--);
248               }
249            }
250         }
251
252         // Clear the cached matrix size
253         mSize = null;
254      }
255   }
256
257   //--------------------------------------------------------------------------
258   public void removeKeys(Collection<K> inKeys)
259   {
260      if (CollectionUtil.hasValues(inKeys))
261      {
262         for (K key : inKeys)
263         {
264            removeKey(key);
265         }
266      }
267   }
268
269   //--------------------------------------------------------------------------
270   public void changeKey(K inOldKey, K inNewKey)
271   {
272      MatrixRow matrixRow = mKeyMap.get(inOldKey);
273      if (matrixRow != null)
274      {
275         matrixRow.setKey(inNewKey);
276         mKeyMap.remove(inOldKey);
277         mKeyMap.put(inNewKey, matrixRow);
278
279         // If any of the cached smallest values are associated with the changed key, change them
280         if (mSmallestValues != null)
281         {
282            for (int i = 0; i < mSmallestValues.size(); i++)
283            {
284               MatrixCell<K, K, V> matrixCell = mSmallestValues.get(i);
285               if (matrixCell.getRowKey().equals(inOldKey))
286               {
287                  matrixCell.setRowKey(inNewKey);
288               }
289               else if (matrixCell.getColKey().equals(inOldKey))
290               {
291                  matrixCell.setColKey(inNewKey);
292               }
293            }
294         }
295      }
296   }
297
298   //--------------------------------------------------------------------------
299   public MatrixCell<K, K, V> getCellWithSmallestValue()
300   {
301      return getCellWithSmallestValue(true);
302   }
303
304   //--------------------------------------------------------------------------
305   public MatrixCell<K, K, V> getNonIdentityCellWithSmallestValue()
306   {
307      return getCellWithSmallestValue(false);
308   }
309
310   //--------------------------------------------------------------------------
311   @Override
312   public SymmetricMatrix<K, V> clone()
313   {
314      SymmetricMatrix<K, V> clone;
315      try
316      {
317         clone = (SymmetricMatrix<K, V>) super.clone();
318      }
319      catch (CloneNotSupportedException e)
320      {
321         throw new ProgrammingException(e);
322      }
323
324      clone.mKeyMap = new HashMap<>(mKeyMap.size());
325      clone.mMatrix = new ArrayList<>(mMatrix.size());
326      for (MatrixRow matrixRow : mMatrix)
327      {
328         // The map values and the array values are the same
329         // objects and we need to keep it that way
330         matrixRow = matrixRow.clone();
331         clone.mMatrix.add(matrixRow);
332         clone.mKeyMap.put(matrixRow.getKey(), matrixRow);
333      }
334
335      return clone;
336   }
337
338   //--------------------------------------------------------------------------
339   public int size()
340   {
341      if (null == mSize)
342      {
343         int size = 0;
344         if (CollectionUtil.hasValues(mKeyMap))
345         {
346            int numKeys = numKeys();
347            size = ((numKeys * numKeys - numKeys) / 2) + numKeys;
348         }
349
350         mSize = size;
351      }
352
353      return mSize;
354   }
355
356   //--------------------------------------------------------------------------
357   protected int getIndexForKey(K inKey)
358   {
359      MatrixRow matrixRow = mKeyMap.get(inKey);
360      return (matrixRow != null ? matrixRow.getIndex() : -1);
361   }
362
363   //##########################################################################
364   // PRIVATE METHODS
365   //##########################################################################
366
367   //--------------------------------------------------------------------------
368   private MatrixRow innerAddKey(K inKey)
369   {
370      // Create and initialize a matrix row
371      MatrixRow matrixRow = new MatrixRow(inKey, mMatrix.size());
372      mKeyMap.put(inKey, matrixRow);
373      mMatrix.add(matrixRow);
374
375      // Clear the cached size
376      mSize = null;
377
378      return matrixRow;
379   }
380
381   //--------------------------------------------------------------------------
382   private int getMaxKeyLength()
383   {
384      int maxLength = 0;
385      for (K key : mKeyMap.keySet())
386      {
387         if (key.toString().length() > maxLength)
388         {
389            maxLength = key.toString().length();
390         }
391      }
392
393      return maxLength;
394   }
395
396   //--------------------------------------------------------------------------
397   private MatrixCell<K, K, V> getCellWithSmallestValue(boolean inIncludeIdentityCells)
398   {
399      if (null == mSmallestValues
400          || mSmallestValuesIncludeIdentity !=  inIncludeIdentityCells)
401      {
402         mSmallestValues = new SortedList<>(sSmallestValuesCapacity);
403         mSmallestValuesIncludeIdentity = inIncludeIdentityCells;
404      }
405
406      if (0 == mSmallestValues.size())
407      {
408         for (MatrixRow matrixRow : mMatrix)
409         {
410            int numCells = matrixRow.getData().size();
411            for (int colIndex = 0; colIndex < numCells; colIndex++)
412            {
413               if (inIncludeIdentityCells
414                   || matrixRow.getIndex() != colIndex)
415               {
416                  V value = matrixRow.getData().get(colIndex);
417                  if (value != null)
418                  {
419                     if (mSmallestValues.size() < sSmallestValuesRetentionSize
420                         || value.compareTo(mSmallestValues.last().getValue()) <= 0)
421                     {
422                        K targetCol = mMatrix.get(colIndex).getKey();
423
424                        mSmallestValues.add(new MatrixCell<>(matrixRow.getKey(), targetCol, value));
425
426                        if (mSmallestValues.size() > sSmallestValuesCapacity - 2)
427                        {
428                           // Truncate the list
429                           mSmallestValues.truncate(sSmallestValuesRetentionSize);
430                        }
431                     }
432                  }
433               }
434            }
435         }
436      }
437
438//      System.out.println(mSmallestValues.get(0).getValue() + " ( " + mSmallestValues.get(0).getRowKey() + " " + mSmallestValues.get(0).getColKey());
439      return (CollectionUtil.hasValues(mSmallestValues) ? mSmallestValues.get(0) : null);
440   }
441
442
443   protected class MatrixRow implements Cloneable
444   {
445      private int mRowIndex;
446      private K mKey;
447      private List<V> mData;
448
449      //-----------------------------------------------------------------------
450      public MatrixRow(K inKey, int inIndex)
451      {
452         mKey = inKey;
453         mRowIndex = inIndex;
454
455         initializeData();
456      }
457
458      //-----------------------------------------------------------------------
459      public void setKey(K inKey)
460      {
461         mKey = inKey;
462      }
463
464      //-----------------------------------------------------------------------
465      public K getKey()
466      {
467         return mKey;
468      }
469
470      //-----------------------------------------------------------------------
471      public void decrementIndex()
472      {
473         mRowIndex--;
474      }
475
476      //-----------------------------------------------------------------------
477      public int getIndex()
478      {
479         return mRowIndex;
480      }
481
482      //-----------------------------------------------------------------------
483      public void setData(List<V> inData)
484      {
485         mData = inData;
486      }
487
488      //-----------------------------------------------------------------------
489      public List<V> getData()
490      {
491         return mData;
492      }
493
494      //-----------------------------------------------------------------------
495      private void initializeData()
496      {
497         mData = new ArrayList<>(mRowIndex + 1);
498         for (int i = 0; i < mRowIndex + 1; i++)
499         {
500            mData.add(null);
501         }
502      }
503
504      //--------------------------------------------------------------------------
505      @Override
506      public MatrixRow clone()
507      {
508         MatrixRow clone;
509         try
510         {
511            clone = (MatrixRow) super.clone();
512         }
513         catch (CloneNotSupportedException e)
514         {
515            throw new ProgrammingException(e);
516         }
517
518         clone.mData = new ArrayList<>(mData);
519
520         return clone;
521      }
522
523   }
524
525}