001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.ce.measure;
021
022import com.google.common.collect.Multiset;
023import com.google.common.collect.TreeMultiset;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.Map;
028import java.util.Set;
029import java.util.TreeMap;
030import javax.annotation.CheckForNull;
031import org.sonar.api.utils.KeyValueFormat;
032
033/**
034 * Utility to build a distribution based on defined ranges
035 * <p>
036 * <p>An example of usage : you wish to record the percentage of lines of code that belong to method
037 * with pre-defined ranges of complexity.
038 *
039 * @since 5.2
040 */
041public class RangeDistributionBuilder {
042
043  private Multiset<Number> distributionSet;
044  private boolean isEmpty = true;
045  private Number[] bottomLimits;
046  private boolean isValid = true;
047
048  public RangeDistributionBuilder() {
049    // Nothing to be done here, bottom limits will be automatically calculated when adding the first value
050  }
051
052  /**
053   * RangeDistributionBuilder for a defined range
054   * Each entry is initialized at zero
055   *
056   * @param bottomLimits the bottom limits of ranges to be used
057   */
058  public RangeDistributionBuilder(Number[] bottomLimits) {
059    init(bottomLimits);
060  }
061
062  /**
063   * Increments an entry by 1
064   *
065   * @param value the value to use to pick the entry to increment
066   */
067  public RangeDistributionBuilder add(Number value) {
068    return add(value, 1);
069  }
070
071  /**
072   * Increments an entry
073   *
074   * @param value the value to use to pick the entry to increment
075   * @param count the number by which to increment
076   */
077  public RangeDistributionBuilder add(Number value, int count) {
078    if (greaterOrEqualsThan(value, bottomLimits[0])) {
079      addValue(value, count);
080      isEmpty = false;
081    }
082    return this;
083  }
084
085  /**
086   * Adds an existing Distribution to the current one.
087   * It will create the entries if they don't exist.
088   * Can be used to add the values of children resources for example
089   * <br>
090   * The returned distribution will be invalidated in case the given value does not use the same bottom limits
091   *
092   * @param data the data to add to the current one
093   */
094  public RangeDistributionBuilder add(String data) {
095    Map<Double, Double> map = KeyValueFormat.parse(data, KeyValueFormat.newDoubleConverter(), KeyValueFormat.newDoubleConverter());
096    if (bottomLimits == null) {
097      Number[] limits = map.keySet().toArray(new Number[map.size()]);
098      init(limits);
099
100    } else if (!areSameLimits(bottomLimits, map.keySet())) {
101      isValid = false;
102    }
103
104    if (isValid) {
105      for (Map.Entry<Double, Double> entry : map.entrySet()) {
106        addLimitCount(entry.getKey(), entry.getValue().intValue());
107      }
108    }
109    return this;
110  }
111
112  private void init(Number[] bottomLimits) {
113    this.bottomLimits = new Number[bottomLimits.length];
114    System.arraycopy(bottomLimits, 0, this.bottomLimits, 0, this.bottomLimits.length);
115    Arrays.sort(this.bottomLimits);
116    changeDoublesToInts();
117    distributionSet = TreeMultiset.create(NumberComparator.INSTANCE);
118  }
119
120  private void changeDoublesToInts() {
121    for (Number bottomLimit : bottomLimits) {
122      if (NumberComparator.INSTANCE.compare(bottomLimit.intValue(), bottomLimit.doubleValue()) != 0) {
123        // it's not only ints
124        return;
125      }
126    }
127    for (int i = 0; i < bottomLimits.length; i++) {
128      bottomLimits[i] = bottomLimits[i].intValue();
129    }
130  }
131
132  private static boolean areSameLimits(Number[] bottomLimits, Set<Double> limits) {
133    if (limits.size() == bottomLimits.length) {
134      for (Number l : bottomLimits) {
135        if (!limits.contains(l.doubleValue())) {
136          return false;
137        }
138      }
139      return true;
140    }
141    return false;
142  }
143
144  private RangeDistributionBuilder addLimitCount(Number limit, int count) {
145    for (Number bottomLimit : bottomLimits) {
146      if (NumberComparator.INSTANCE.compare(bottomLimit.doubleValue(), limit.doubleValue()) == 0) {
147        addValue(limit, count);
148        isEmpty = false;
149        return this;
150      }
151    }
152    isValid = false;
153    return this;
154  }
155
156  private void addValue(Number value, int count) {
157    for (int i = bottomLimits.length - 1; i >= 0; i--) {
158      if (greaterOrEqualsThan(value, bottomLimits[i])) {
159        this.distributionSet.add(bottomLimits[i], count);
160        return;
161      }
162    }
163  }
164
165  /**
166   * @return whether the current object is empty or not
167   */
168  public boolean isEmpty() {
169    return isEmpty;
170  }
171
172  /**
173   * Used to build a measure from the current object
174   *
175   * @return the built measure
176   */
177  @CheckForNull
178  public String build() {
179    if (isValid) {
180      return KeyValueFormat.format(toMap());
181    }
182    return null;
183  }
184
185  private Map<Number, Integer> toMap() {
186    if (bottomLimits == null || bottomLimits.length == 0) {
187      return Collections.emptyMap();
188    }
189    Map<Number, Integer> map = new TreeMap<>();
190    for (Number value : bottomLimits) {
191      map.put(value, distributionSet.count(value));
192    }
193    return map;
194  }
195
196  private static boolean greaterOrEqualsThan(Number n1, Number n2) {
197    return NumberComparator.INSTANCE.compare(n1, n2) >= 0;
198  }
199
200  private enum NumberComparator implements Comparator<Number> {
201    INSTANCE;
202
203    @Override
204    public int compare(Number n1, Number n2) {
205      return Double.compare(n1.doubleValue(), n2.doubleValue());
206    }
207  }
208
209}