content: /*
   *  Copyright (C) 2010-2020 Stichting Akvo (Akvo Foundation)
   *
   *  This file is part of Akvo FLOW.
   *
   *  Akvo FLOW is free software: you can redistribute it and modify it under the terms of
   *  the GNU Affero General Public License (AGPL) as published by the Free Software Foundation,
   *  either version 3 of the License or any later version.
   *
   *  Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
   *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   *  See the GNU Affero General Public License included below for more details.
   *
   *  The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>.
   */
  
  package org.waterforpeople.mapping.dataexport;
  
  import java.io.File;
  import java.text.DecimalFormat;
  import java.text.NumberFormat;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  
  import com.fasterxml.jackson.core.type.TypeReference;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import org.akvo.flow.domain.DataUtils;
  import org.akvo.flow.util.FlowJsonObjectReader;
  import org.json.JSONArray;
  import org.json.JSONObject;
  import org.waterforpeople.mapping.app.gwt.client.survey.OptionContainerDto;
  import org.waterforpeople.mapping.app.gwt.client.survey.QuestionDto;
  import org.waterforpeople.mapping.app.gwt.client.survey.QuestionOptionDto;
  import org.waterforpeople.mapping.app.gwt.client.survey.QuestionDto.QuestionType;
  import org.waterforpeople.mapping.app.gwt.client.survey.QuestionGroupDto;
  import org.waterforpeople.mapping.app.web.dto.SurveyRestRequest;
  import org.waterforpeople.mapping.dataexport.service.BulkDataServiceClient;
  
  import com.gallatinsystems.framework.dataexport.applet.AbstractDataExporter;
  
  /**
   * This exporter will write the survey "descriptive statistics" report to a file. These stats
   * include a breakdown of question response frequencies for each question in a survey.
   *
   * @author Christopher Fagiani
   */
  public class SurveySummaryExporter extends AbstractDataExporter {
  
      private static final Logger log = Logger.getLogger(SurveySummaryExporter.class.getName());
  
      public static final String RESPONSE_KEY = "dtoList";
      private static final String SERVLET_URL = "/surveyrestapi";
      private static final NumberFormat PCT_FMT = new DecimalFormat("0.00");
      protected static final String[] ROLLUP_QUESTIONS = {
              "Sector/Cell",
              "Department", "Province", "Municipality", "Region", "District",
              "Traditional Authority (TA)", "Sub-Traditional Authority (Sub-TA)",
              "County", "Sub County", "Sector", "Cell", "Gran Panchayet",
              "Block", "State", "Micro-Region"
      };
      protected static final Map<String, String[]> ROLLUP_MAP;
      static {
          ROLLUP_MAP = new HashMap<String, String[]>();
          ROLLUP_MAP.put("IN", new String[] {
                  "State", "District", "Block",
                  "Gran Panchayet"
          });
          ROLLUP_MAP.put("HN", new String[] {
                  "Department", "Municipality",
                  "Sector"
          });
          ROLLUP_MAP.put("GT", new String[] {
                  "Department", "Municipality"
          });
          ROLLUP_MAP.put("DR", new String[] {
                  "Province", "Municipality"
          });
          ROLLUP_MAP.put("NI", new String[] {
                  "Department", "Municipality",
                  "Micro-Region"
          });
          ROLLUP_MAP.put("BO", new String[] {
                  "Department", "Municipality",
                  "District"
          });
          ROLLUP_MAP.put("PE", new String[] {
                  "Region", "Province", "District"
          });
          ROLLUP_MAP.put("EC", new String[] {
                  "Province", "Municipality"
          });
          ROLLUP_MAP.put("MW", new String[] {
                  "District",
                  "Traditional Authority (TA)",
                  "Sub-Traditional Authority (Sub-TA)"
          });
          ROLLUP_MAP.put("RW", new String[] {
                  "District", "Sector", "Cell"
          });
          ROLLUP_MAP.put("UG",
                  new String[] {
                          "District", "County", "Sub County"
                  });
      }
      protected List<QuestionGroupDto> orderedGroupList;
  
      /**
       * the ordered list of "rollup questions" (i.e. so we know how to build the drill-downs)
       */
      protected List<QuestionDto> rollupOrder;
  
      @Override
      public void export(Map<String, String> criteria, File fileName,
              String serverBase, Map<String, String> options) {
      }
  
      protected SummaryModel buildDataModel(String surveyId, String serverBase, String apiKey)
              throws Exception {
          SummaryModel model = new SummaryModel();
          Map<String, String> instanceMap = BulkDataServiceClient
                  .fetchInstanceIds(surveyId, serverBase, apiKey, false, null, null, null);
          for (String instanceId : instanceMap.keySet()) {
              // TODO!!
              Map<String, String> responseMap = new HashMap<>();
              // Map<String, String> responseMap = BulkDataServiceClient
              // .fetchQuestionResponses(instanceId, serverBase, apiKey);
              Set<String> rollups = null;
              if (rollupOrder != null && rollupOrder.size() > 0) {
                  rollups = formRollupStrings(responseMap);
              }
              for (Entry<String, String> entry : responseMap.entrySet()) {
                  model.tallyResponse(entry.getKey(), rollups, entry.getValue(), null);
              }
          }
          return model;
      }
  
      /**
       * builds the keys to use for roll-ups. So if the rollupOrder contains 2 questions, say State
       * and District, it will form strings that look like: "<StateResponse>" and
       * "<StateResponse>|<DistrictResponse>"
       *
       * @param responseMap
       * @return
       */
      protected Set<String> formRollupStrings(Map<String, String> responseMap) {
          Set<String> rollups = new HashSet<String>();
          for (int j = 0; j < rollupOrder.size(); j++) {
              String rollup = "";
              int count = 0;
              for (int i = 0; i < rollupOrder.size() - j; i++) {
                  String val = responseMap.get(rollupOrder.get(i).getKeyId().toString());
                  if (val != null && val.trim().length() > 0) {
                      //Extract from JSON, if any
                      String jsonval = DataUtils.jsonResponsesToPipeSeparated(val);
                      if (jsonval.length() > 0) {
                          val = jsonval;
                      }
  
                      if (count > 0) {
                          rollup += "|";
                      }
                      rollup += val;
                      count++;
                  }
              }
              rollups.add(rollup);
          }
          return rollups;
  
      }
  
      /**
       * loads just enough question data to generate the simplest report
       * As side effect, creates and populates orderedGroupList and rollupOrder.
       * @param surveyId
       * @param performRollups
       * @param serverBase
       * @param apiKey
       * @return
       * @throws Exception
       */
      protected Map<QuestionGroupDto, List<QuestionDto>> loadAllQuestions(
              String surveyId,
              boolean performRollups,
              String serverBase,
              String apiKey)
              throws Exception {
          Map<QuestionGroupDto, List<QuestionDto>> questionMap = new HashMap<>();
          //we need the ordering of groups and questions in them; fetching in nested loops is inefficient so
          //we fetch them all at once and sort them ourself
          orderedGroupList = fetchQuestionGroups(serverBase, surveyId, apiKey);
          List<QuestionDto> allQuestions = fetchQuestionsOfSurvey(serverBase, surveyId, apiKey); //unordered
          Map<Long, List<QuestionDto>> idMap = new HashMap<>();
          for (QuestionGroupDto group : orderedGroupList) {
              List<QuestionDto> questions = new ArrayList<>();
              idMap.put(group.getKeyId(), questions);
          }
          // Sort them into the right lists
          for (QuestionDto q:allQuestions) {
              List<QuestionDto> myList = idMap.get(q.getQuestionGroupId());
              if (myList != null) { //in case db is inconsistent
                  myList.add(q);
              }
          }
          // Lists complete, now we can sort and visit each in order
          rollupOrder = new ArrayList<QuestionDto>();
          for (QuestionGroupDto group : orderedGroupList) {
              List<QuestionDto> questions = idMap.get(group.getKeyId());
              Collections.sort(questions, new Comparator<QuestionDto>() {
                  @Override
                  public int compare(QuestionDto o1, QuestionDto o2) {
                      //order should never be null, but accidents happen...
                      int v1 = o1.getOrder() != null ? o1.getOrder() : 0;
                      int v2 = o2.getOrder() != null ? o2.getOrder() : 0;
                      return v1-v2;
                  }
              });
  
              if (performRollups && questions != null) {
                  for (QuestionDto q : questions) {
                      for (int i = 0; i < ROLLUP_QUESTIONS.length; i++) {
                          if (ROLLUP_QUESTIONS[i].equalsIgnoreCase(q.getText())) {
                              rollupOrder.add(q);
                          }
                      }
                  }
              }
              questionMap.put(group, questions);
          }
  
          return questionMap;
      }
  
      /**
       * calls the server to augment the data already loaded in each QuestionDto in the map
       * with variable names, if not set already
       *
       * @param surveyId
       * @param serverBase
       * @param questionMap map of lists of questionDtos keyed by id
       * @param apiKey
       */
      protected void loadVariableNames(
              String surveyId,
              String serverBase,
              Map<QuestionGroupDto, List<QuestionDto>> questionMap,
              String apiKey) {
  
          Map<Long, QuestionDto> questionsById = new HashMap<>();
          for (List<QuestionDto> qList : questionMap.values()) {
              for (QuestionDto q : qList) {
                  questionsById.put(q.getKeyId(), q);
              }
          }
  
          try {
              List<QuestionDto> dsQuestions = fetchQuestionsOfSurvey(serverBase, surveyId, apiKey); //unordered
              for (QuestionDto sdDto : dsQuestions) {
                  QuestionDto dto = questionsById.get(sdDto.getKeyId());
                  if (dto != null && dto.getVariableName() == null) {
                      dto.setVariableName(sdDto.getVariableName());
                  }
              }
  
          } catch (Exception e) {
              log.log(Level.SEVERE, "Could not fetch question variableNames", e);
          }
      }
  
      /**
       * calls the server to augment the data already loaded in each QuestionDto in the map
       * with minimal option info, no translations
       *
       * @param questionMap questionDtos keyed by id
       * @param apiKey
       */
      protected void loadQuestionOptions(
              String surveyId,
              String serverBase,
              Map<QuestionGroupDto, List<QuestionDto>> questionMap,
              String apiKey) {
  
          try {
              Map<Long, QuestionDto> questionsById = new HashMap<>();
              for (List<QuestionDto> qList : questionMap.values()) {
                  for (QuestionDto q : qList) {
                      questionsById.put(q.getKeyId(), q);
                  }
              }
  
              List<QuestionOptionDto> optList =
                      BulkDataServiceClient.fetchSurveyQuestionOptions(surveyId, serverBase, apiKey);
              //add them to the container of their question
              for (QuestionOptionDto o:optList) {
                  QuestionDto q = questionsById.get(o.getQuestionId());
                  if (q != null) {
                      //May need to create an OptionContainer to hold them
                      OptionContainerDto container = q.getOptionContainerDto();
                      if (container == null) {
                          container = new OptionContainerDto();
                          q.setOptionContainerDto(container);
                          //For symmetry, since these exist in two places:
                          container.setAllowMultipleFlag(q.getAllowMultipleFlag());
                          container.setAllowOtherFlag(q.getAllowOtherFlag());
                      }
                      container.addQuestionOption(o);
                  }
              }
          } catch (Exception e) {
              log.log(Level.SEVERE, "Could not fetch question options", e);
          }
      }
  
  
      protected List<QuestionDto> fetchQuestions(String serverBase, Long groupId, String apiKey)
              throws Exception {
  
          return parseQuestions(BulkDataServiceClient.fetchDataFromServer(
                  serverBase + SERVLET_URL, "action="
                          + SurveyRestRequest.LIST_QUESTION_ACTION + "&"
                          + SurveyRestRequest.QUESTION_GROUP_ID_PARAM + "="
                          + groupId, true, apiKey));
      }
  
      protected List<QuestionDto> fetchQuestionsOfSurvey(String serverBase, String surveyId, String apiKey)
              throws Exception {
  
          return parseQuestions(BulkDataServiceClient.fetchDataFromServer(
                  serverBase + SERVLET_URL, "action="
                          + SurveyRestRequest.LIST_SURVEY_QUESTIONS_ACTION + "&"
                          + SurveyRestRequest.SURVEY_ID_PARAM + "="
                          + surveyId, true, apiKey));
      }
  
      protected List<QuestionGroupDto> fetchQuestionGroups(String serverBase,
              String surveyId, String apiKey) throws Exception {
          return parseQuestionGroups(BulkDataServiceClient.fetchDataFromServer(
                  serverBase + SERVLET_URL, "action="
                          + SurveyRestRequest.LIST_GROUP_ACTION + "&"
                          + SurveyRestRequest.SURVEY_ID_PARAM + "=" + surveyId,
                  true, apiKey));
      }
  
      protected List<QuestionGroupDto> parseQuestionGroups(String response)
              throws Exception {
          List<QuestionGroupDto> dtoList = new ArrayList<QuestionGroupDto>();
          JSONArray arr = getJsonArray(response);
          if (arr != null) {
              for (int i = 0; i < arr.length(); i++) {
                  JSONObject json = arr.getJSONObject(i);
                  if (json != null) {
                      QuestionGroupDto dto = new QuestionGroupDto();
                      try {
                          if (json.has("code")) {
                              dto.setCode(json.getString("code"));
                          }
                          if (json.has("keyId")) {
                              dto.setKeyId(json.getLong("keyId"));
                          }
                          if (json.has("repeatable")) {
                              dto.setRepeatable(json.getBoolean("repeatable"));
                          }
                          if (json.has("order")) {
                              dto.setOrder(json.getInt("order"));
                          }
                          dtoList.add(dto);
                      } catch (Exception e) {
                          log.log(Level.SEVERE, "Error in json parsing: " + e.getMessage(), e);
                      }
                  }
              }
          }
          return dtoList;
      }
  
  
      /**
       * parses questions using an object mapper
       * @param response
       * @return
       * @throws Exception
       */
      protected List<QuestionDto> parseQuestions(String response) throws Exception {
          final FlowJsonObjectReader jsonReader = new FlowJsonObjectReader();
          final TypeReference<QuestionDto> listItemTypeReference = new TypeReference<QuestionDto>(){};
          final List<QuestionDto> qList = jsonReader.readDtoListObject(response, listItemTypeReference);
          return qList;
      }
  
  
      /**
       * converts the string into a JSON array object.
       */
      protected JSONArray getJsonArray(String response) throws Exception {
          log.finest("response: " + response);
          if (response != null) {
              JSONObject json = new JSONObject(response);
              if (json != null) {
                  return json.getJSONArray(RESPONSE_KEY);
              }
          }
          return null;
      }
  
  
      protected class SummaryModel {
          // contains the map of questionIds to all valid responses
          private Map<String, List<String>> responseMap;
          // list of all sectors encountered
          private List<String> rollupList;
          // map of frequency counts of a response. the key is the packed value of
          // questionId+sector+response
          private Map<String, Long> sectorCountMap;
          // map of totals for all responses in a quesitonId+sector
          private Map<String, Long> sectorTotalMap;
          private Map<String, Long> responseCountMap;
          private Map<String, Long> responseTotalMap;
          // map of question to stats value
          private Map<String, DescriptiveStats> statMap;
          private Map<String, Map<String, DescriptiveStats>> sectorStatMap;
  
          public SummaryModel() {
              responseMap = new HashMap<String, List<String>>();
              rollupList = new ArrayList<String>();
              sectorCountMap = new HashMap<String, Long>();
              sectorTotalMap = new HashMap<String, Long>();
              responseCountMap = new HashMap<String, Long>();
              responseTotalMap = new HashMap<String, Long>();
              statMap = new HashMap<String, DescriptiveStats>();
              sectorStatMap = new HashMap<String, Map<String, DescriptiveStats>>();
          }
  
          public void tallyResponse(String questionId, Set<String> rollups,
                  String response, QuestionDto qDto) {
  
              if (qDto != null && QuestionType.NUMBER == qDto.getType()) {
                  // for NUMBER questions, if decimals-allowed changes
                  // during survey, "1" and "1.0" should be tallied together
                  if (response.endsWith(".0")) {
                      response = response.substring(0, response.length() - 2);
                  }
              }
              addResponse(questionId, response);
              addRollup(rollups);
              incrementCount(questionId, rollups, response);
              updateStats(questionId, rollups, response);
          }
  
          private void updateStats(String questionId, Set<String> rollups,
                  String response) {
              if (statMap.get(questionId) == null) {
                  DescriptiveStats stats = new DescriptiveStats();
                  stats.addSample(response);
                  statMap.put(questionId, stats);
              } else {
                  statMap.get(questionId).addSample(response);
              }
              if (rollups != null) {
                  for (String sector : rollups) {
                      if (sector != null) {
                          if (sectorStatMap.get(sector) == null) {
                              Map<String, DescriptiveStats> secStats = new HashMap<String, DescriptiveStats>();
                              sectorStatMap.put(sector, secStats);
                              DescriptiveStats stats = new DescriptiveStats();
                              stats.addSample(response);
                              secStats.put(questionId, stats);
                          } else {
                              if (sectorStatMap.get(sector).get(questionId) == null) {
                                  DescriptiveStats stats = new DescriptiveStats();
                                  stats.addSample(response);
                                  sectorStatMap.get(sector)
                                          .put(questionId, stats);
                              } else {
                                  sectorStatMap.get(sector).get(questionId)
                                          .addSample(response);
                              }
                          }
                      }
                  }
              }
          }
  
          private void incrementCount(String questionId, Set<String> rollups,
                  String response) {
              if (rollups != null) {
                  for (String sector : rollups) {
                      incrementValue(questionId + sector + response,
                              sectorCountMap);
                      incrementValue(questionId + sector, sectorTotalMap);
                  }
              }
              incrementValue(questionId + response, responseCountMap);
              incrementValue(questionId, responseTotalMap);
          }
  
          private void incrementValue(String key, Map<String, Long> map) {
              Long val = map.get(key);
              if (val == null) {
                  val = 1L;
              } else {
                  val++;
              }
              map.put(key, val);
          }
  
          private void addRollup(Set<String> rollups) {
              if (rollups != null) {
                  for (String sector : rollups) {
                      if (sector != null && sector.trim().length() > 0
                              && !rollupList.contains(sector.trim())) {
                          rollupList.add(sector.trim());
  
                      }
                  }
              }
          }
  
          private void addResponse(String questionId, String response) {
              List<String> responses = responseMap.get(questionId);
              if (responses == null) {
                  responses = new ArrayList<String>();
                  responseMap.put(questionId, responses);
              }
              if (!responses.contains(response)) {
                  responses.add(response);
              }
          }
  
          public String outputQuestion(String groupName, String questionText,
                  String questionId, boolean isRolledUp) {
              String result = null;
  
              if (isRolledUp) {
                  StringBuilder buffer = new StringBuilder();
                  for (String sector : rollupList) {
                      buffer.append(outputResponses(groupName, questionText,
                              sector, questionId, isRolledUp));
                  }
                  result = buffer.toString();
              } else {
                  result = outputResponses(groupName, questionText, "",
                          questionId, isRolledUp);
              }
              return result;
          }
  
          private String outputResponses(String groupName, String questionText,
                  String sector, String questionId, boolean isRolledUp) {
              StringBuilder buffer = new StringBuilder();
              if (responseMap.get(questionId) != null) {
                  for (String response : responseMap.get(questionId)) {
                      Long count = null;
                      if (isRolledUp) {
                          count = sectorCountMap.get(questionId + sector
                                  + response);
                      } else {
                          count = responseCountMap.get(questionId + response);
                      }
                      String countString = "0";
                      String pctString = "0";
                      if (count != null) {
                          Long total = null;
                          if (isRolledUp) {
                              total = sectorTotalMap.get(questionId + sector);
                          } else {
                              total = responseTotalMap.get(questionId);
                          }
                          pctString = PCT_FMT.format((double) count
                                  / (double) total);
                          countString = count.toString();
                      }
                      buffer.append(groupName).append("\t").append(questionText)
                              .append("\t");
                      if (isRolledUp) {
                          buffer.append(sector).append("\t");
                      }
                      buffer.append(response).append("\t").append(countString)
                              .append("\t").append(pctString).append("\t")
                              .append(statMap.get(questionId).getStatsString())
                              .append("\n");
                  }
              }
              return buffer.toString();
          }
  
          public Map<String, Long> getResponseCountsForQuestion(Long questionId,
                  String sector) {
              List<String> responses = responseMap.get(questionId.toString());
              Map<String, Long> countMap = new HashMap<String, Long>();
              if (responses != null) {
                  for (String resp : responses) {
                      Long count = null;
                      if (sector == null) {
                          count = responseCountMap.get(questionId + resp);
                      } else {
                          count = sectorCountMap.get(questionId + sector + resp);
                      }
                      countMap.put(resp, count != null ? count : new Long(0));
                  }
              }
              return countMap;
          }
  
          public DescriptiveStats getDescriptiveStatsForQuestion(Long questionId,
                  String sector) {
              if (sector == null) {
                  return statMap.get(questionId.toString());
              } else {
                  if (sectorStatMap.get(sector) != null) {
                      return sectorStatMap.get(sector).get(questionId.toString());
                  } else {
                      return null;
                  }
              }
          }
  
          public List<String> getSectorList() {
              return rollupList;
          }
  
      }
  
      protected class DescriptiveStats {
          private double max;
          private double min;
          private double mean;
          private double sumSqMean;
          private int sampleCount;
          private List<Double> valueList;
          private boolean isSorted;
  
          public DescriptiveStats() {
              mean = 0d;
              sampleCount = 0;
              sumSqMean = 0d;
              isSorted = false;
              valueList = new ArrayList<Double>();
              max = Double.MIN_VALUE;
              min = Double.MAX_VALUE;
          }
  
          public int getSampleCount() {
              return sampleCount;
          }
  
          public void addSample(String stringVal) {
              double val = Double.MIN_VALUE;
              try {
                  val = Double.parseDouble(stringVal);
              } catch (Exception e) {
                  return;
              }
              sampleCount++;
              if (val > max) {
                  max = val;
              }
              if (val < min) {
                  min = val;
              }
              double delta = val - mean;
              mean = mean + (delta / sampleCount);
              // the sumSqMean calc uses the newly updated value for mean
              sumSqMean = sumSqMean + delta * (val - mean);
              isSorted = false;
              valueList.add(val);
          }
  
          public double getMean() {
              return mean;
          }
  
          public double getRange() {
              return max - min;
          }
  
          public double getVariance() {
              return sumSqMean / (sampleCount - 1d);
          }
  
          public double getMedian() {
              if (!isSorted) {
                  Collections.sort(valueList);
                  isSorted = true;
              }
              if (valueList.size() % 2 == 1) {
                  return valueList.get((int) Math.floor(valueList.size() / 2));
              } else {
                  Double lowerVal = valueList.get(valueList.size() / 2);
                  Double upperVal = valueList.get(valueList.size() / 2 - 1);
                  return (lowerVal + upperVal) / 2;
              }
          }
  
          public Double getMode() {
              if (!isSorted) {
                  Collections.sort(valueList);
                  isSorted = true;
              }
              int maxOccur = 0;
              int curOccur = 0;
              Double maxOccurValue = null;
              Double lastValue = null;
              for (Double val : valueList) {
                  if (lastValue == null || !val.equals(lastValue)) {
                      if (curOccur > maxOccur) {
                          maxOccur = curOccur;
                          maxOccurValue = lastValue;
                      }
                      lastValue = val;
                      curOccur = 1;
                  } else if (val.equals(lastValue)) {
                      curOccur++;
                  }
              }
              if (maxOccurValue == null) {
                  maxOccurValue = lastValue;
              }
              return maxOccurValue;
          }
  
          public double getStandardDeviation() {
              return Math.sqrt(getVariance());
          }
  
          public double getStandardError() {
              return Math.sqrt(getVariance() / sampleCount);
          }
  
          /**
           * outputs stats in the following order (tab delimited): Mean, Median, Mode, Std Dev, Std
           * Err, Range
           *
           * @return
           */
          public String getStatsString() {
              StringBuilder builder = new StringBuilder();
              if (sampleCount > 0) {
                  builder.append(getMean()).append("\t").append(getMedian())
                          .append("\t").append(getMode()).append("\t")
                          .append(getStandardDeviation()).append("\t")
                          .append(getStandardError()).append("\t")
                          .append(getRange());
              } else {
                  builder.append("\t\t\t\t\t");
              }
              return builder.toString();
          }
  
          public double getMax() {
              return max;
          }
  
          public double getMin() {
              return min;
          }
  
      }
  }
  