diff --git a/python/plugins/processing/algs/qgis/StatisticsByCategories.py b/python/plugins/processing/algs/qgis/StatisticsByCategories.py old mode 100644 new mode 100755 index dfe0097f06f..9e04aa20c9c --- a/python/plugins/processing/algs/qgis/StatisticsByCategories.py +++ b/python/plugins/processing/algs/qgis/StatisticsByCategories.py @@ -28,6 +28,8 @@ __revision__ = '$Format:%H$' from qgis.core import (QgsProcessingParameterFeatureSource, QgsStatisticalSummary, + QgsDateTimeStatisticalSummary, + QgsStringStatisticalSummary, QgsFeatureRequest, QgsProcessingParameterField, QgsProcessingParameterFeatureSink, @@ -36,13 +38,16 @@ from qgis.core import (QgsProcessingParameterFeatureSource, QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeature, - QgsFeatureSink) + QgsFeatureSink, + QgsProcessing, + NULL) from qgis.PyQt.QtCore import QVariant from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm +from collections import defaultdict + class StatisticsByCategories(QgisAlgorithm): - INPUT = 'INPUT' VALUES_FIELD_NAME = 'VALUES_FIELD_NAME' CATEGORIES_FIELD_NAME = 'CATEGORIES_FIELD_NAME' @@ -56,13 +61,16 @@ class StatisticsByCategories(QgisAlgorithm): def initAlgorithm(self, config=None): self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, - self.tr('Input vector layer'))) + self.tr('Input vector layer'), + types=[QgsProcessing.TypeVector])) self.addParameter(QgsProcessingParameterField(self.VALUES_FIELD_NAME, - self.tr('Field to calculate statistics on'), - parentLayerParameterName=self.INPUT, type=QgsProcessingParameterField.Numeric)) + self.tr( + 'Field to calculate statistics on (if empty, only count is calculated)'), + parentLayerParameterName=self.INPUT, optional=True)) self.addParameter(QgsProcessingParameterField(self.CATEGORIES_FIELD_NAME, - self.tr('Field with categories'), - parentLayerParameterName=self.INPUT, type=QgsProcessingParameterField.Any)) + self.tr('Field(s) with categories'), + parentLayerParameterName=self.INPUT, + type=QgsProcessingParameterField.Any, allowMultiple=True)) self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Statistics by category'))) @@ -75,49 +83,213 @@ class StatisticsByCategories(QgisAlgorithm): def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) value_field_name = self.parameterAsString(parameters, self.VALUES_FIELD_NAME, context) - category_field_name = self.parameterAsString(parameters, self.CATEGORIES_FIELD_NAME, context) + category_field_names = self.parameterAsFields(parameters, self.CATEGORIES_FIELD_NAME, context) value_field_index = source.fields().lookupField(value_field_name) - category_field_index = source.fields().lookupField(category_field_name) + if value_field_index >= 0: + value_field = source.fields().at(value_field_index) + else: + value_field = None + category_field_indexes = [source.fields().lookupField(n) for n in category_field_names] - features = source.getFeatures(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)) - total = 100.0 / source.featureCount() if source.featureCount() else 0 - values = {} + # generate output fields + fields = QgsFields() + for c in category_field_indexes: + fields.append(source.fields().at(c)) + + def addField(name): + """ + Adds a field to the output, keeping the same data type as the value_field + """ + field = value_field + field.setName(name) + fields.append(field) + + if value_field is None: + field_type = 'none' + fields.append(QgsField('count', QVariant.Int)) + elif value_field.isNumeric(): + field_type = 'numeric' + fields.append(QgsField('count', QVariant.Int)) + fields.append(QgsField('unique', QVariant.Int)) + fields.append(QgsField('min', QVariant.Double)) + fields.append(QgsField('max', QVariant.Double)) + fields.append(QgsField('range', QVariant.Double)) + fields.append(QgsField('sum', QVariant.Double)) + fields.append(QgsField('mean', QVariant.Double)) + fields.append(QgsField('median', QVariant.Double)) + fields.append(QgsField('stddev', QVariant.Double)) + fields.append(QgsField('minority', QVariant.Double)) + fields.append(QgsField('majority', QVariant.Double)) + fields.append(QgsField('q1', QVariant.Double)) + fields.append(QgsField('q3', QVariant.Double)) + fields.append(QgsField('iqr', QVariant.Double)) + elif value_field.type() in (QVariant.Date, QVariant.Time, QVariant.DateTime): + field_type = 'datetime' + fields.append(QgsField('count', QVariant.Int)) + fields.append(QgsField('unique', QVariant.Int)) + fields.append(QgsField('empty', QVariant.Int)) + fields.append(QgsField('filled', QVariant.Int)) + # keep same data type for these fields + addField('min') + addField('max') + else: + field_type = 'string' + fields.append(QgsField('count', QVariant.Int)) + fields.append(QgsField('unique', QVariant.Int)) + fields.append(QgsField('empty', QVariant.Int)) + fields.append(QgsField('filled', QVariant.Int)) + # keep same data type for these fields + addField('min') + addField('max') + fields.append(QgsField('min_length', QVariant.Int)) + fields.append(QgsField('max_length', QVariant.Int)) + fields.append(QgsField('mean_length', QVariant.Double)) + + request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry) + if value_field is not None: + attrs = [value_field_index] + else: + attrs = [] + attrs.extend(category_field_indexes) + request.setSubsetOfAttributes(attrs) + features = source.getFeatures(request) + total = 50.0 / source.featureCount() if source.featureCount() else 0 + if field_type == 'none': + values = defaultdict(lambda: 0) + else: + values = defaultdict(list) for current, feat in enumerate(features): if feedback.isCanceled(): break feedback.setProgress(int(current * total)) attrs = feat.attributes() - try: - value = float(attrs[value_field_index]) - cat = attrs[category_field_index] - if cat not in values: - values[cat] = [] - values[cat].append(value) - except: - pass - - fields = QgsFields() - fields.append(source.fields().at(category_field_index)) - fields.append(QgsField('min', QVariant.Double)) - fields.append(QgsField('max', QVariant.Double)) - fields.append(QgsField('mean', QVariant.Double)) - fields.append(QgsField('stddev', QVariant.Double)) - fields.append(QgsField('sum', QVariant.Double)) - fields.append(QgsField('count', QVariant.Int)) + cat = tuple([attrs[c] for c in category_field_indexes]) + if field_type == 'none': + values[cat] += 1 + continue + if field_type == 'numeric': + if attrs[value_field_index] == NULL: + continue + else: + value = float(attrs[value_field_index]) + elif field_type == 'string': + if attrs[value_field_index] == NULL: + value = '' + else: + value = str(attrs[value_field_index]) + elif attrs[value_field_index] == NULL: + value = NULL + else: + value = attrs[value_field_index] + values[cat].append(value) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem()) - stat = QgsStatisticalSummary(QgsStatisticalSummary.Min | QgsStatisticalSummary.Max | - QgsStatisticalSummary.Mean | QgsStatisticalSummary.StDevSample | - QgsStatisticalSummary.Sum | QgsStatisticalSummary.Count) - - for (cat, v) in list(values.items()): - stat.calculate(v) - f = QgsFeature() - f.setAttributes([cat, stat.min(), stat.max(), stat.mean(), stat.sampleStDev(), stat.sum(), stat.count()]) - sink.addFeature(f, QgsFeatureSink.FastInsert) + if field_type == 'none': + self.saveCounts(values, sink, feedback) + elif field_type == 'numeric': + self.calcNumericStats(values, sink, feedback) + elif field_type == 'datetime': + self.calcDateTimeStats(values, sink, feedback) + else: + self.calcStringStats(values, sink, feedback) return {self.OUTPUT: dest_id} + + def saveCounts(self, values, sink, feedback): + total = 50.0 / len(values) if values else 0 + current = 0 + for cat, v in values.items(): + if feedback.isCanceled(): + break + + feedback.setProgress(int(current * total) + 50) + f = QgsFeature() + f.setAttributes(list(cat) + [v]) + sink.addFeature(f, QgsFeatureSink.FastInsert) + current += 1 + + def calcNumericStats(self, values, sink, feedback): + stat = QgsStatisticalSummary() + + total = 50.0 / len(values) if values else 0 + current = 0 + for cat, v in values.items(): + if feedback.isCanceled(): + break + + feedback.setProgress(int(current * total) + 50) + + stat.calculate(v) + f = QgsFeature() + f.setAttributes(list(cat) + [stat.count(), + stat.variety(), + stat.min(), + stat.max(), + stat.range(), + stat.sum(), + stat.mean(), + stat.median(), + stat.stDev(), + stat.minority(), + stat.majority(), + stat.firstQuartile(), + stat.thirdQuartile(), + stat.interQuartileRange()]) + + sink.addFeature(f, QgsFeatureSink.FastInsert) + current += 1 + + def calcDateTimeStats(self, values, sink, feedback): + stat = QgsDateTimeStatisticalSummary() + + total = 50.0 / len(values) if values else 0 + current = 0 + for cat, v in values.items(): + if feedback.isCanceled(): + break + + feedback.setProgress(int(current * total) + 50) + + stat.calculate(v) + f = QgsFeature() + f.setAttributes(list(cat) + [stat.count(), + stat.countDistinct(), + stat.countMissing(), + stat.count() - stat.countMissing(), + stat.statistic(QgsDateTimeStatisticalSummary.Min), + stat.statistic(QgsDateTimeStatisticalSummary.Max) + ]) + + sink.addFeature(f, QgsFeatureSink.FastInsert) + current += 1 + + def calcStringStats(self, values, sink, feedback): + stat = QgsStringStatisticalSummary() + + total = 50.0 / len(values) if values else 0 + current = 0 + for cat, v in values.items(): + if feedback.isCanceled(): + break + + feedback.setProgress(int(current * total) + 50) + + stat.calculate(v) + f = QgsFeature() + f.setAttributes(list(cat) + [stat.count(), + stat.countDistinct(), + stat.countMissing(), + stat.count() - stat.countMissing(), + stat.min(), + stat.max(), + stat.minLength(), + stat.maxLength(), + stat.meanLength() + ]) + + sink.addFeature(f, QgsFeatureSink.FastInsert) + current += 1 diff --git a/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py b/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py deleted file mode 100644 index e6c1885ee33..00000000000 --- a/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py +++ /dev/null @@ -1,47 +0,0 @@ -##Vector analysis=group - -#inputs - -##Input=source -##Fields=field multiple Input -##Frequency=sink table - - -from processing.tools.vector import TableWriter -from collections import defaultdict -from qgis.core import QgsProcessingUtils, QgsFields, QgsField, QgsWkbTypes, QgsFeature -from qgis.PyQt.QtCore import QVariant -from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException - -inputFields = Input.fields() -fieldIdxs = [] -out_fields = QgsFields() -for f in Fields: - idx = inputFields.indexFromName(f) - if idx == -1: - raise GeoAlgorithmExecutionException('Field not found:' + f) - fieldIdxs.append(idx) - out_fields.append(inputFields.at(idx)) - -out_fields.append(QgsField('FREQ', QVariant.Int)) - -(sink, Frequency) = self.parameterAsSink(parameters, 'Frequency', context, - out_fields) - -counts = {} -feats = Input.getFeatures() -nFeats = Input.featureCount() -counts = defaultdict(int) -for i, feat in enumerate(feats): - feedback.setProgress(int(100 * i / nFeats)) - if feedback.isCanceled(): - break - - attrs = feat.attributes() - clazz = tuple([attrs[i] for i in fieldIdxs]) - counts[clazz] += 1 - -for c in counts: - f = QgsFeature() - f.setAttributes(list(c) + [counts[c]]) - sink.addFeature(f) diff --git a/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py b/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py deleted file mode 100644 index 5335c66e3f9..00000000000 --- a/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py +++ /dev/null @@ -1,52 +0,0 @@ -##Vector analysis=group - -# inputs - - -##input=source -##class_field=field input -##value_field=field input -##N_unique_values=sink - - -from qgis.PyQt.QtCore import QVariant -from qgis.core import QgsFeature, QgsField, QgsProcessingUtils - -fields = input.fields() -fields.append(QgsField('UNIQ_COUNT', QVariant.Int)) - -(sink, N_unique_values) = self.parameterAsSink(parameters, 'N_unique_values', context, - fields, input.wkbType(), input.sourceCrs()) - - -class_field_index = input.fields().lookupField(class_field) -value_field_index = input.fields().lookupField(value_field) - -outFeat = QgsFeature() -classes = {} -feats = input.getFeatures() -nFeat = input.featureCount() -for n, inFeat in enumerate(feats): - if feedback.isCanceled(): - break - feedback.setProgress(int(100 * n / nFeat)) - attrs = inFeat.attributes() - clazz = attrs[class_field_index] - value = attrs[value_field_index] - if clazz not in classes: - classes[clazz] = [] - if value not in classes[clazz]: - classes[clazz].append(value) - -feats = input.getFeatures() -for n, inFeat in enumerate(feats): - if feedback.isCanceled(): - break - feedback.setProgress(int(100 * n / nFeat)) - inGeom = inFeat.geometry() - outFeat.setGeometry(inGeom) - attrs = inFeat.attributes() - clazz = attrs[class_field_index] - attrs.append(len(classes[clazz])) - outFeat.setAttributes(attrs) - sink.addFeature(outFeat) diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs new file mode 100644 index 00000000000..1e740b97e92 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs @@ -0,0 +1,48 @@ + + + stats_by_cat_date + stats_by_cat_date + 100 + + 4 + + + date + date + String + 10 + + + count + count + Integer + + + unique + unique + Integer + + + empty + empty + Integer + + + filled + filled + Integer + + + min + min + String + 10 + + + max + max + String + 10 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml new file mode 100644 index 00000000000..27c0f84cb3b --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml @@ -0,0 +1,50 @@ + + + missing + + + + 2016/11/30 + 1 + 1 + 0 + 1 + 2016/11/30 + 2016/11/30 + + + + + 2016/11/10 + 1 + 1 + 0 + 1 + 2016/11/10 + 2016/11/10 + + + + + 1 + 0 + 1 + 0 + + + + + 2014/11/30 + 1 + 1 + 0 + 1 + 2014/11/30 + 2014/11/30 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs new file mode 100644 index 00000000000..1069d98aeb8 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs @@ -0,0 +1,86 @@ + + + stats_by_cat_float + stats_by_cat_float + 100 + + 5 + + + name + name + String + 2 + + + count + count + Integer + + + unique + unique + Integer + + + min + min + Real + + + max + max + Real + + + range + range + Real + + + sum + sum + Real + + + mean + mean + Real + + + median + median + Real + + + stddev + stddev + Real + + + minority + minority + Real + + + majority + majority + Real + + + q1 + q1 + Real + + + q3 + q3 + Real + + + iqr + iqr + Real + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml new file mode 100644 index 00000000000..1fa21ebeadc --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml @@ -0,0 +1,103 @@ + + + missing + + + + aa + 2 + 2 + 3.33 + 44.123456 + 40.793456 + 47.453456 + 23.726728 + 23.726728 + 20.396728 + 3.33 + 3.33 + 3.33 + 44.123456 + 40.793456 + + + + + dd + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + + + bb + 4 + 1 + 0.123 + 0.123 + 0 + 0.492 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0.123 + 0 + + + + + 1 + 1 + -100291.43213 + -100291.43213 + 0 + -100291.43213 + -100291.43213 + -100291.43213 + 0 + -100291.43213 + -100291.43213 + -100291.43213 + -100291.43213 + 0 + + + + + cc + 1 + 1 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0.123 + 0 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs new file mode 100644 index 00000000000..b8719d76b93 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs @@ -0,0 +1,26 @@ + + + stats_by_cat_no_value + stats_by_cat_no_value + 100 + + 6 + + + intval + intval + Integer + + + name + name + String + 2 + + + count + count + Integer + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml new file mode 100644 index 00000000000..5590c2625a4 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml @@ -0,0 +1,48 @@ + + + missing + + + + 1 + aa + 2 + + + + + dd + 2 + + + + + 1 + bb + 3 + + + + + 120 + 1 + + + + + cc + 1 + + + + + 2 + bb + 1 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs new file mode 100644 index 00000000000..0fb2358c926 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs @@ -0,0 +1,62 @@ + + + stats_by_cat_string + stats_by_cat_string + 100 + + 4 + + + intval + intval + Integer + + + count + count + Integer + + + unique + unique + Integer + + + empty + empty + Integer + + + filled + filled + Integer + + + min + min + String + 2 + + + max + max + String + 2 + + + min_length + min_length + Integer + + + max_length + max_length + Integer + + + mean_length + mean_length + Integer + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml new file mode 100644 index 00000000000..600e578de7d --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml @@ -0,0 +1,64 @@ + + + missing + + + + 1 + 5 + 2 + 0 + 5 + aa + bb + 2 + 2 + 2 + + + + + 3 + 2 + 0 + 3 + cc + dd + 2 + 2 + 2 + + + + + 120 + 1 + 1 + 1 + 0 + + + 0 + 0 + 0 + + + + + 2 + 1 + 1 + 0 + 1 + bb + bb + 2 + 2 + 2 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs new file mode 100644 index 00000000000..348c809ee01 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs @@ -0,0 +1,91 @@ + + + stats_by_cat_two_fields + stats_by_cat_two_fields + 100 + + 6 + + + intval + intval + Integer + + + name + name + String + 2 + + + count + count + Integer + + + unique + unique + Integer + + + min + min + Real + + + max + max + Real + + + range + range + Real + + + sum + sum + Real + + + mean + mean + Real + + + median + median + Real + + + stddev + stddev + Real + + + minority + minority + Real + + + majority + majority + Real + + + q1 + q1 + Real + + + q3 + q3 + Real + + + iqr + iqr + Real + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml new file mode 100644 index 00000000000..e8668bfdc2b --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml @@ -0,0 +1,126 @@ + + + missing + + + + 1 + aa + 2 + 2 + 3.33 + 44.123456 + 40.793456 + 47.453456 + 23.726728 + 23.726728 + 20.396728 + 3.33 + 3.33 + 3.33 + 44.123456 + 40.793456 + + + + + dd + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + + + 1 + bb + 3 + 1 + 0.123 + 0.123 + 0 + 0.369 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0.123 + 0 + + + + + 120 + 1 + 1 + -100291.43213 + -100291.43213 + 0 + -100291.43213 + -100291.43213 + -100291.43213 + 0 + -100291.43213 + -100291.43213 + -100291.43213 + -100291.43213 + 0 + + + + + cc + 1 + 1 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0.123 + 0 + + + + + 2 + bb + 1 + 1 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0 + 0.123 + 0.123 + 0.123 + 0.123 + 0 + + + diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs index ba663caa0c0..99c98501a5b 100644 --- a/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs +++ b/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs @@ -11,6 +11,16 @@ id2 Integer + + count + count + Integer + + + unique + unique + Integer + min min @@ -21,24 +31,54 @@ max Integer + + range + range + Integer + + + sum + sum + Integer + mean mean Real + + median + median + Real + stddev stddev Real - sum - sum + minority + minority Integer - count - count + majority + majority + Integer + + + q1 + q1 + Integer + + + q3 + q3 + Integer + + + iqr + iqr Integer diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_category.gml b/python/plugins/processing/tests/testdata/expected/stats_by_category.gml index 4647a986f75..7fcd964d673 100644 --- a/python/plugins/processing/tests/testdata/expected/stats_by_category.gml +++ b/python/plugins/processing/tests/testdata/expected/stats_by_category.gml @@ -9,34 +9,58 @@ 2 + 2 + 2 1 4 - 2.5 - 2.12132034355964 + 3 5 - 2 + 2.5 + 2.5 + 1.5 + 1 + 1 + 1 + 4 + 3 1 + 2 + 2 2 5 - 3.5 - 2.12132034355964 + 3 7 - 2 + 3.5 + 3.5 + 1.5 + 2 + 2 + 2 + 5 + 3 0 + 5 + 5 3 9 - 6.6 - 2.30217288664427 + 6 33 - 5 + 6.6 + 7 + 2.0591260281974 + 3 + 3 + 6 + 8 + 2 diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 0bcff7fcfa1..887813449e4 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -3476,3 +3476,98 @@ tests: name: expected/collect_two_fields.gml type: vector + + - algorithm: qgis:statisticsbycategories + name: Stats by cat (float field) + params: + CATEGORIES_FIELD_NAME: + - name + INPUT: + name: dissolve_polys.gml + type: vector + VALUES_FIELD_NAME: floatval + results: + OUTPUT: + name: expected/stats_by_cat_float.gml + type: vector + pk: name + compare: + fields: + fid: skip + + - algorithm: qgis:statisticsbycategories + name: Stats by cat (string field) + params: + CATEGORIES_FIELD_NAME: + - intval + INPUT: + name: dissolve_polys.gml + type: vector + VALUES_FIELD_NAME: name + results: + OUTPUT: + name: expected/stats_by_cat_string.gml + type: vector + pk: intval + compare: + fields: + fid: skip + + - algorithm: qgis:statisticsbycategories + name: Stats by cat (two category fields) + params: + CATEGORIES_FIELD_NAME: + - intval + - name + INPUT: + name: dissolve_polys.gml + type: vector + VALUES_FIELD_NAME: floatval + results: + OUTPUT: + name: expected/stats_by_cat_two_fields.gml + type: vector + pk: + - intval + - name + compare: + fields: + fid: skip + + - algorithm: qgis:statisticsbycategories + name: Stats by cat (no value field) + params: + CATEGORIES_FIELD_NAME: + - intval + - name + INPUT: + name: dissolve_polys.gml + type: vector + results: + OUTPUT: + name: expected/stats_by_cat_no_value.gml + type: vector + pk: + - intval + - name + compare: + fields: + fid: skip + + - algorithm: qgis:statisticsbycategories + name: Stats by cat (date field) + params: + CATEGORIES_FIELD_NAME: + - date + INPUT: + name: custom/datetimes.tab + type: vector + VALUES_FIELD_NAME: date + results: + OUTPUT: + name: expected/stats_by_cat_date.gml + type: vector + pk: date + compare: + fields: + fid: skip \ No newline at end of file diff --git a/python/testing/__init__.py b/python/testing/__init__.py index 7b8359de5ee..4cea7c414cf 100644 --- a/python/testing/__init__.py +++ b/python/testing/__init__.py @@ -105,7 +105,11 @@ class TestCase(_TestCase): def sort_by_pk_or_fid(f): if 'pk' in kwargs and kwargs['pk'] is not None: - return f[kwargs['pk']] + key = kwargs['pk'] + if isinstance(key, list) or isinstance(key, tuple): + return [f[k] for k in key] + else: + return f[kwargs['pk']] else: return f.id() diff --git a/src/core/processing/qgsprocessingparameters.cpp b/src/core/processing/qgsprocessingparameters.cpp index 59561855195..ac1265060a6 100644 --- a/src/core/processing/qgsprocessingparameters.cpp +++ b/src/core/processing/qgsprocessingparameters.cpp @@ -2301,6 +2301,9 @@ bool QgsProcessingParameterField::checkValueIsAcceptable( const QVariant &input, { if ( !mAllowMultiple ) return false; + + if ( input.toList().isEmpty() && !( mFlags & FlagOptional ) ) + return false; } else if ( input.type() == QVariant::String ) { diff --git a/tests/src/core/testqgsprocessing.cpp b/tests/src/core/testqgsprocessing.cpp index de11fe2c9cc..0bc2c122043 100644 --- a/tests/src/core/testqgsprocessing.cpp +++ b/tests/src/core/testqgsprocessing.cpp @@ -2258,6 +2258,8 @@ void TestQgsProcessing::parameterLayerList() QVERIFY( !def->checkValueIsAcceptable( true ) ); QVERIFY( !def->checkValueIsAcceptable( 5 ) ); QVERIFY( !def->checkValueIsAcceptable( "layer12312312" ) ); + QVERIFY( !def->checkValueIsAcceptable( QVariantList() ) ); + QVERIFY( !def->checkValueIsAcceptable( QStringList() ) ); QVERIFY( def->checkValueIsAcceptable( QStringList() << "layer12312312" << "layerB" ) ); QVERIFY( def->checkValueIsAcceptable( QVariantList() << "layer12312312" << "layerB" ) ); QVERIFY( !def->checkValueIsAcceptable( "" ) ); @@ -3183,6 +3185,8 @@ void TestQgsProcessing::parameterField() QVERIFY( def->checkValueIsAcceptable( QVariantList() << "a" << "b" ) ); QVERIFY( !def->checkValueIsAcceptable( "" ) ); QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); + QVERIFY( !def->checkValueIsAcceptable( QStringList() ) ); + QVERIFY( !def->checkValueIsAcceptable( QVariantList() ) ); params.insert( "non_optional", QString( "a;b" ) ); fields = QgsProcessingParameters::parameterAsFields( def.get(), params, context );