files.new_contents: #Copyright 2008 Orbitz WorldWide
  #
  #Licensed under the Apache License, Version 2.0 (the "License");
  #you may not use this file except in compliance with the License.
  #You may obtain a copy of the License at
  #
  #   http://www.apache.org/licenses/LICENSE-2.0
  #
  #Unless required by applicable law or agreed to in writing, software
  #distributed under the License is distributed on an "AS IS" BASIS,
  #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  #See the License for the specific language governing permissions and
  #limitations under the License.
  
  # make / work consistently between python 2.x and 3.x
  # https://www.python.org/dev/peps/pep-0238/
  from __future__ import division
  
  import math
  import random
  import re
  import time
  
  from datetime import datetime, timedelta
  from functools import reduce
  from six.moves import range, zip
  import six
  try:
    from itertools import izip, izip_longest
  except ImportError:
    # Python 3
    from itertools import zip_longest as izip_longest
    izip = zip
  from os import environ
  
  from django.conf import settings
  
  from graphite.errors import NormalizeEmptyResultError
  from graphite.events import models
  from graphite.functions import SeriesFunction, ParamTypes, Param
  from graphite.logger import log
  from graphite.render.attime import getUnitString, parseTimeOffset, parseATTime, SECONDS_STRING, MINUTES_STRING, HOURS_STRING, DAYS_STRING, WEEKS_STRING, MONTHS_STRING, YEARS_STRING
  from graphite.render.evaluator import evaluateTarget
  from graphite.render.grammar import grammar
  from graphite.storage import STORE
  from graphite.util import epoch, epoch_to_dt, timestamp, deltaseconds
  
  # XXX format_units() should go somewhere else
  if environ.get('READTHEDOCS'):
    format_units = lambda *args, **kwargs: (0,'')
  else:
    from graphite.render.glyph import format_units
    from graphite.render.datalib import TimeSeries
  
  NAN = float('NaN')
  INF = float('inf')
  DAY = 86400
  HOUR = 3600
  MINUTE = 60
  
  #Utility functions
  
  
  def safeSum(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return sum(safeValues)
  
  
  def safeDiff(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      values = list(map(lambda x: x*-1, safeValues[1:]))
      values.insert(0, safeValues[0])
      return sum(values)
  
  
  def safeLen(values):
    return len([v for v in values if v is not None])
  
  
  def safeDiv(a, b):
    if a is None: return None
    if b in (0,None): return None
    return a / b
  
  
  def safePow(a, b):
    if a is None: return None
    if b is None: return None
    try:
      result = math.pow(a, b)
    except (ValueError, OverflowError):
      return None
    return result
  
  
  def safeMul(*factors):
    if None in factors:
      return None
  
    factors = [float(x) for x in factors]
    product = reduce(lambda x,y: x*y, factors)
    return product
  
  
  def safeSubtract(a,b):
      if a is None or b is None: return None
      return float(a) - float(b)
  
  
  def safeAvg(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return sum(safeValues) / len(safeValues)
  
  
  def safeMedian(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      sortedVals = sorted(safeValues)
      mid = len(sortedVals) // 2
      if len(sortedVals) % 2 == 0:
        return float(sortedVals[mid-1] + sortedVals[mid]) / 2
      else:
        return sortedVals[mid]
  
  
  def safeStdDev(a):
    sm = safeSum(a)
    ln = safeLen(a)
    avg = safeDiv(sm,ln)
    if avg is None: return None
    sum = 0
    safeValues = [v for v in a if v is not None]
    for val in safeValues:
       sum = sum + (val - avg) * (val - avg)
    return math.sqrt(sum/ln)
  
  
  def safeLast(values):
    for v in reversed(values):
      if v is not None: return v
  
  
  def safeMin(values, default=None):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return min(safeValues)
    else:
      return default
  
  
  def safeMax(values, default=None):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return max(safeValues)
    else:
      return default
  
  
  def safeMap(function, values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return [function(x) for x in safeValues]
  
  
  def safeAbs(value):
    if value is None: return None
    return abs(value)
  
  
  # In Py2, None < (any integer) . Use -inf for same sorting on Python 3
  def keyFunc(func):
    def safeFunc(values):
      val = func(values)
      return val if val is not None else -INF
    return safeFunc
  
  
  # Greatest common divisor
  def gcd(a, b):
    if b == 0:
      return a
    return gcd(b, a%b)
  
  
  # Least common multiple
  def lcm(a, b):
    if a == b: return a
    if a < b: (a, b) = (b, a) #ensure a > b
    return a // gcd(a,b) * b
  
  
  # check list of values against xFilesFactor
  def xffValues(values, xFilesFactor):
    if not values:
      return False
    return xff(len([v for v in values if v is not None]), len(values), xFilesFactor)
  
  
  def xff(nonNull, total, xFilesFactor=None):
    if not nonNull or not total:
      return False
    return nonNull / total >= (xFilesFactor if xFilesFactor is not None else settings.DEFAULT_XFILES_FACTOR)
  
  
  def getNodeOrTag(series, n, pathExpression=None):
    try:
      # if n is an integer use it as a node, otherwise catch ValueError and treat it as a tag
      if n == int(n):
        # first split off any tags, then list of nodes is name split on .
        return (pathExpression or series.name).split(';', 2)[0].split('.')[n]
    except (ValueError, IndexError):
      # if node isn't an integer or isn't found then try it as a tag name
      pass
    # return tag value, default to '' if not found
    return series.tags.get(str(n), '')
  
  
  def aggKey(series, nodes, pathExpression=None):
    # if series.name looks like it includes a function, use the first path expression
    if pathExpression is None and series.name[-1] == ')':
      pathExpression = _getFirstPathExpression(series.name)
    return '.'.join([getNodeOrTag(series, n, pathExpression) for n in nodes])
  
  
  def normalize(seriesLists):
    if seriesLists:
      seriesList = reduce(lambda L1,L2: L1+L2,seriesLists)
      if seriesList:
        step = reduce(lcm,[s.step for s in seriesList])
        for s in seriesList:
          s.consolidate( step // s.step )
        start = min([s.start for s in seriesList])
        end = max([s.end for s in seriesList])
        end -= (end - start) % step
        return (seriesList,start,end,step)
    raise NormalizeEmptyResultError()
  
  
  def matchSeries(seriesList1, seriesList2):
    assert len(seriesList2) == len(seriesList1), "The number of series in each argument must be the same"
    return izip(sorted(seriesList1, key=lambda a: a.name), sorted(seriesList2, key=lambda a: a.name))
  
  
  def formatPathExpressions(seriesList):
    # remove duplicates
    pathExpressions = []
    [pathExpressions.append(s.pathExpression) for s in seriesList if not pathExpressions.count(s.pathExpression)]
    return ','.join(pathExpressions)
  
  
  # Series Functions
  
  aggFuncs = {
    'average': safeAvg,
    'median': safeMedian,
    'sum': safeSum,
    'min': safeMin,
    'max': safeMax,
    'diff': safeDiff,
    'stddev': safeStdDev,
    'count': safeLen,
    'range': lambda row: safeSubtract(safeMax(row), safeMin(row)),
    'multiply': lambda row: safeMul(*row),
    'last': safeLast,
  }
  
  aggFuncAliases = {
    'rangeOf': aggFuncs['range'],
    'avg': aggFuncs['average'],
    'total': aggFuncs['sum'],
    'current': aggFuncs['last'],
  }
  
  aggFuncNames = sorted(aggFuncs.keys())
  
  
  def getAggFunc(func, rawFunc=None):
    if func in aggFuncs:
      return aggFuncs[func]
    if func in aggFuncAliases:
      return aggFuncAliases[func]
    raise ValueError('Unsupported aggregation function: %s' % (rawFunc or func))
  
  
  def aggregate(requestContext, seriesList, func, xFilesFactor=None):
    """
    Aggregate series using the specified function.
  
    Example:
  
    .. code-block:: none
  
      &target=aggregate(host.cpu-[0-7].cpu-{user,system}.value, "sum")
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-{user,system}.value)
  
    This function can be used with aggregation functions ``average``, ``median``, ``sum``, ``min``,
    ``max``, ``diff``, ``stddev``, ``count``, ``range``, ``multiply`` & ``last``.
    """
    # strip Series from func if func was passed like sumSeries
    rawFunc = func
    if func[-6:] == 'Series':
      func = func[:-6]
  
    consolidationFunc = getAggFunc(func, rawFunc)
  
    # if seriesList is empty then just short-circuit
    if not seriesList:
      return []
  
    # if seriesList is a single series then wrap it for normalize
    if isinstance(seriesList[0], TimeSeries):
      seriesList = [seriesList]
  
    try:
      (seriesList, start, end, step) = normalize(seriesList)
    except NormalizeEmptyResultError:
      return []
    xFilesFactor = xFilesFactor if xFilesFactor is not None else requestContext.get('xFilesFactor')
    name = "%sSeries(%s)" % (func, formatPathExpressions(seriesList))
    values = ( consolidationFunc(row) if xffValues(row, xFilesFactor) else None for row in izip_longest(*seriesList) )
    tags = seriesList[0].tags
    for series in seriesList:
      tags = {tag: tags[tag] for tag in tags if tag in series.tags and tags[tag] == series.tags[tag]}
    if 'name' not in tags:
      tags['name'] = name
    tags['aggregatedBy'] = func
    series = TimeSeries(name, start, end, step, values, xFilesFactor=xFilesFactor, tags=tags)
  
    return [series]
  
  
  aggregate.group = 'Combine'
  aggregate.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('func', ParamTypes.aggFunc, required=True, options=aggFuncNames),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def sumSeries(requestContext, *seriesLists):
    """
    Short form: sum()
  
    This will add metrics together and return the sum at each datapoint. (See
    integral for a sum over time)
  
    Example:
  
    .. code-block:: none
  
      &target=sum(company.server.application*.requestsHandled)
  
    This would show the sum of all requests handled per minute (provided
    requestsHandled are collected once a minute).   If metrics with different
    retention rates are combined, the coarsest metric is graphed, and the sum
    of the other metrics is averaged for the metrics with finer retention rates.
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``sum``.
    """
    return aggregate(requestContext, seriesLists, 'sum')
  
  
  sumSeries.group = 'Combine'
  sumSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def sumSeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call sumSeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=sumSeriesWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-user.value)&target=sumSeries(host.cpu-[0-7].cpu-system.value)
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``sum``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'sum', *position)
  
  
  sumSeriesWithWildcards.group = 'Combine'
  sumSeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def averageSeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call averageSeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=averageSeriesWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=averageSeries(host.*.cpu-user.value)&target=averageSeries(host.*.cpu-system.value)
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``average``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'average', *position)
  
  
  averageSeriesWithWildcards.group = 'Combine'
  averageSeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def multiplySeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call multiplySeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=multiplySeriesWithWildcards(web.host-[0-7].{avg-response,total-request}.value, 2)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=multiplySeries(web.host-0.{avg-response,total-request}.value)&target=multiplySeries(web.host-1.{avg-response,total-request}.value)...
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``multiply``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'multiply', *position)
  
  
  multiplySeriesWithWildcards.group = 'Combine'
  multiplySeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def aggregateWithWildcards(requestContext, seriesList, func, *positions):
    """
    Call aggregator after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=aggregateWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, "sum", 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-user.value)&target=sumSeries(host.cpu-[0-7].cpu-system.value)
      # or
      &target=aggregate(host.cpu-[0-7].cpu-user.value,"sum")&target=aggregate(host.cpu-[0-7].cpu-system.value,"sum")
  
    This function can be used with all aggregation functions supported by
    :py:func:`aggregate <aggregate>`: ``average``, ``median``, ``sum``, ``min``, ``max``, ``diff``,
    ``stddev``, ``range`` & ``multiply``.
  
    This complements :py:func:`groupByNodes <groupByNodes>` which takes a list of nodes that must match in each group.
    """
    metaSeries = {}
    keys = []
    for series in seriesList:
      key = '.'.join(map(lambda x: x[1], filter(lambda i: i[0] not in positions, enumerate(series.name.split('.')))))
      if key not in metaSeries:
        metaSeries[key] = [series]
        keys.append(key)
      else:
        metaSeries[key].append(series)
    for key in metaSeries.keys():
      metaSeries[key] = aggregate(requestContext, metaSeries[key], func)[0]
      metaSeries[key].name = key
    return [ metaSeries[key] for key in keys ]
  
  
  aggregateWithWildcards.group = 'Combine'
  aggregateWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('func', ParamTypes.aggFunc, required=True, options=aggFuncNames),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def diffSeries(requestContext, *seriesLists):
    """
    Subtracts series 2 through n from series 1.
  
    Example:
  
    .. code-block:: none
  
      &target=diffSeries(service.connections.total,service.connections.failed)
  
    To diff a series and a constant, one should use offset instead of (or in
    addition to) diffSeries
  
    Example:
  
    .. code-block:: none
  
      &target=offset(service.connections.total,-5)
  
      &target=offset(diffSeries(service.connections.total,service.connections.failed),-4)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``diff``.
    """
    return aggregate(requestContext, seriesLists, 'diff')
  
  
  diffSeries.group = 'Combine'
  diffSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def averageSeries(requestContext, *seriesLists):
    """
    Short Alias: avg()
  
    Takes one metric or a wildcard seriesList.
    Draws the average value of all metrics passed at each time.
  
    Example:
  
    .. code-block:: none
  
      &target=averageSeries(company.server.*.threads.busy)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``average``.
    """
    return aggregate(requestContext, seriesLists, 'average')
  
  
  averageSeries.group = 'Combine'
  averageSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def stddevSeries(requestContext, *seriesLists):
    """
  
    Takes one metric or a wildcard seriesList.
    Draws the standard deviation of all metrics passed at each time.
  
    Example:
  
    .. code-block:: none
  
      &target=stddevSeries(company.server.*.threads.busy)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``stddev``.
    """
    return aggregate(requestContext, seriesLists, 'stddev')
  
  
  stddevSeries.group = 'Combine'
  stddevSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def minSeries(requestContext, *seriesLists):
    """
    Takes one metric or a wildcard seriesList.
    For each datapoint from each metric passed in, pick the minimum value and graph it.
  
    Example:
  
    .. code-block:: none
  
      &target=minSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``min``.
    """
    return aggregate(requestContext, seriesLists, 'min')
  
  
  minSeries.group = 'Combine'
  minSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def maxSeries(requestContext, *seriesLists):
    """
    Takes one metric or a wildcard seriesList.
    For each datapoint from each metric passed in, pick the maximum value and graph it.
  
    Example:
  
    .. code-block:: none
  
      &target=maxSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``max``.
    """
    return aggregate(requestContext, seriesLists, 'max')
  
  
  maxSeries.group = 'Combine'
  maxSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def rangeOfSeries(requestContext, *seriesLists):
    """
    Takes a wildcard seriesList.
    Distills down a set of inputs into the range of the series
  
    Example:
  
    .. code-block:: none
  
        &target=rangeOfSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``rangeOf``.
    """
    return aggregate(requestContext, seriesLists, 'rangeOf')
  
  
  rangeOfSeries.group = 'Combine'
  rangeOfSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def percentileOfSeries(requestContext, seriesList, n, interpolate=False):
    """
    percentileOfSeries returns a single series which is composed of the n-percentile
    values taken across a wildcard series at each point. Unless `interpolate` is
    set to True, percentile values are actual values contained in one of the
    supplied series.
    """
    if n <= 0:
      raise ValueError('The requested percent is required to be greater than 0')
  
    # if seriesList is empty then just short-circuit
    if not seriesList:
      return []
  
    name = 'percentileOfSeries(%s,%g)' % (seriesList[0].pathExpression, n)
    (start, end, step) = normalize([seriesList])[1:]
    values = [ _getPercentile(row, n, interpolate) for row in izip_longest(*seriesList) ]
    resultSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
  
    return [resultSeries]
  
  
  percentileOfSeries.group = 'Combine'
  percentileOfSeries.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('n', ParamTypes.integer, required=True),
    Param('interpolate', ParamTypes.boolean, default=False),
  ]
  
  
  def keepLastValue(requestContext, seriesList, limit = INF):
    """
    Takes one metric or a wildcard seriesList, and optionally a limit to the number of 'None' values to skip over.
    Continues the line with the last received value when gaps ('None' values) appear in your data, rather than breaking your line.
  
    Example:
  
    .. code-block:: none
  
      &target=keepLastValue(Server01.connections.handled)
      &target=keepLastValue(Server01.connections.handled, 10)
  
    """
    for series in seriesList:
      series.name = "keepLastValue(%s)" % (series.name)
      series.pathExpression = series.name
      consecutiveNones = 0
      lastVal = None
      for i,value in enumerate(series):
        if value is None:
          consecutiveNones += 1
        else:
           if 0 < consecutiveNones <= limit and lastVal is not None:
             # If a non-None value is seen before the limit of Nones is hit,
             # backfill all the missing datapoints with the last known value.
             for index in range(i - consecutiveNones, i):
               series[index] = lastVal
  
           consecutiveNones = 0
           lastVal = value
  
      # If the series ends with some None values, try to backfill a bit to cover it.
      if 0 < consecutiveNones <= limit and lastVal is not None:
        for index in range(len(series) - consecutiveNones, len(series)):
          series[index] = lastVal
  
    return seriesList
  
  
  keepLastValue.group = 'Transform'
  keepLastValue.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('limit', ParamTypes.integer, default='INF'),
  ]
  
  
  def interpolate(requestContext, seriesList, limit = INF):
    """
    Takes one metric or a wildcard seriesList, and optionally a limit to the number of 'None' values to skip over.
    Continues the line with the last received value when gaps ('None' values) appear in your data, rather than breaking your line.
  
    Example:
  
    .. code-block:: none
  
      &target=interpolate(Server01.connections.handled)
      &target=interpolate(Server01.connections.handled, 10)
  
    """
    for series in seriesList:
      series.name = "interpolate(%s)" % (series.name)
      series.pathExpression = series.name
      consecutiveNones = 0
      for i,value in enumerate(series):
        series[i] = value
  
        # No 'keeping' can be done on the first value because we have no idea
        # what came before it.
        if i == 0:
          continue
  
        if value is None:
          consecutiveNones += 1
        elif consecutiveNones == 0: # have a value but no need to interpolate
          continue
        elif series[i - consecutiveNones - 1] is None: # have a value but can't interpolate: reset count
          consecutiveNones = 0
          continue
        else: # have a value and can interpolate
          # If a non-None value is seen before the limit of Nones is hit,
          # backfill all the missing datapoints with the last known value.
          if 0 < consecutiveNones <= limit:
            for index in range(i - consecutiveNones, i):
              series[index] = series[i - consecutiveNones - 1] + (index - (i - consecutiveNones -1)) * (value - series[i - consecutiveNones - 1]) / (consecutiveNones + 1)
  
          consecutiveNones = 0
  
      # If the series ends with some None values, try to backfill a bit to cover it.
      # if 0 < consecutiveNones < limit:
      #   for index in xrange(len(series) - consecutiveNones, len(series)):
      #     series[index] = series[len(series) - consecutiveNones - 1]
  
    return seriesList
  
  
  interpolate.group = 'Transform'
  interpolate.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('limit', ParamTypes.integer, default='INF'),
  ]
  
  
  def changed(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList.
    Output 1 when the value changed, 0 when null or the same
  
    Example:
  
    .. code-block:: none
  
      &target=changed(Server01.connections.handled)
  
    """
    for series in seriesList:
      series.name = "changed(%s)" % (series.name)
      series.pathExpression = series.name
      previous = None
      for i,value in enumerate(series):
        if previous is None:
          previous = value
          series[i] = 0
        elif value is not None and previous != value:
          series[i] = 1
          previous = value
        else:
          series[i] = 0
    return seriesList
  
  
  changed.group = 'Special'
  changed.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def asPercent(requestContext, seriesList, total=None, *nodes):
    """
  
    Calculates a percentage of the total of a wildcard series. If `total` is specified,
    each series will be calculated as a percentage of that total. If `total` is not specified,
    the sum of all points in the wildcard series will be used instead.
  
    A list of nodes can optionally be provided, if so they will be used to match series with their
    corresponding totals following the same logic as :py:func:`groupByNodes <groupByNodes>`.
  
    When passing `nodes` the `total` parameter may be a series list or `None`.  If it is `None` then
    for each series in `seriesList` the percentage of the sum of series in that group will be returned.
  
    When not passing `nodes`, the `total` parameter may be a single series, reference the same number
    of series as `seriesList` or be a numeric value.
  
    Example:
  
    .. code-block:: none
  
      # Server01 connections failed and succeeded as a percentage of Server01 connections attempted
      &target=asPercent(Server01.connections.{failed,succeeded}, Server01.connections.attempted)
  
      # For each server, its connections failed as a percentage of its connections attempted
      &target=asPercent(Server*.connections.failed, Server*.connections.attempted)
  
      # For each server, its connections failed and succeeded as a percentage of its connections attemped
      &target=asPercent(Server*.connections.{failed,succeeded}, Server*.connections.attempted, 0)
  
      # apache01.threads.busy as a percentage of 1500
      &target=asPercent(apache01.threads.busy,1500)
  
      # Server01 cpu stats as a percentage of its total
      &target=asPercent(Server01.cpu.*.jiffies)
  
      # cpu stats for each server as a percentage of its total
      &target=asPercent(Server*.cpu.*.jiffies, None, 0)
  
    When using `nodes`, any series or totals that can't be matched will create output series with
    names like ``asPercent(someSeries,MISSING)`` or ``asPercent(MISSING,someTotalSeries)`` and all
    values set to None. If desired these series can be filtered out by piping the result through
    ``|exclude("MISSING")`` as shown below:
  
    .. code-block:: none
  
      &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)
  
      # will produce 3 output series:
      # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected]
      # asPercent(Server2.memory.used,MISSING) [all values will be None]
      # asPercent(MISSING,Server3.memory.total) [all values will be None]
  
      &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)|exclude("MISSING")
  
      # will produce 1 output series:
      # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected]
  
    Each node may be an integer referencing a node in the series name or a string identifying a tag.
  
    .. note::
  
      When `total` is a seriesList, specifying `nodes` to match series with the corresponding total
      series will increase reliability.
  
    """
    normalize([seriesList])
    xFilesFactor=requestContext.get('xFilesFactor')
  
    # if nodes are specified, use them to match series & total
    if nodes:
      keys = []
  
      # group series together by key
      metaSeries = {}
      for series in seriesList:
        key = aggKey(series, nodes)
        if key not in metaSeries:
          metaSeries[key] = [series]
          keys.append(key)
        else:
          metaSeries[key].append(series)
  
      # make list of totals
      totalSeries = {}
      # no total seriesList was specified, sum the values for each group of series
      if total is None:
        for key in keys:
          if len(metaSeries[key]) == 1:
            totalSeries[key] = metaSeries[key][0]
          else:
            name = 'sumSeries(%s)' % formatPathExpressions(metaSeries[key])
            (seriesList,start,end,step) = normalize([metaSeries[key]])
            totalValues = [ safeSum(row) for row in izip_longest(*metaSeries[key]) ]
            totalSeries[key] = TimeSeries(name,start,end,step,totalValues,xFilesFactor=xFilesFactor)
      # total seriesList was specified, sum the values for each group of totals
      elif isinstance(total, list):
        for series in total:
          key = aggKey(series, nodes)
          if key not in totalSeries:
            totalSeries[key] = [series]
            if key not in keys:
              keys.append(key)
          else:
            totalSeries[key].append(series)
  
        for key in totalSeries.keys():
          if len(totalSeries[key]) == 1:
            totalSeries[key] = totalSeries[key][0]
          else:
            name = 'sumSeries(%s)' % formatPathExpressions(totalSeries[key])
            (seriesList,start,end,step) = normalize([totalSeries[key]])
            totalValues = [ safeSum(row) for row in izip_longest(*totalSeries[key]) ]
            totalSeries[key] = TimeSeries(name,start,end,step,totalValues,xFilesFactor=xFilesFactor)
      # trying to use nodes with a total value, which isn't supported because it has no effect
      else:
        raise ValueError('total must be None or a seriesList')
  
      resultList = []
      for key in keys:
        # no series, must have total only
        if key not in metaSeries:
          series2 = totalSeries[key]
          name = "asPercent(%s,%s)" % ('MISSING', series2.name)
          resultValues = [ None for v1 in series2 ]
          resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
          resultList.append(resultSeries)
          continue
  
        for series1 in metaSeries[key]:
          # no total
          if key not in totalSeries:
            name = "asPercent(%s,%s)" % (series1.name, 'MISSING')
            resultValues = [ None for v1 in series1 ]
          # series and total
          else:
            series2 = totalSeries[key]
            name = "asPercent(%s,%s)" % (series1.name, series2.name)
            (seriesList,start,end,step) = normalize([(series1, series2)])
            resultValues = [ safeMul(safeDiv(v1, v2), 100.0) for v1,v2 in izip_longest(series1,series2) ]
  
          resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
          resultList.append(resultSeries)
  
      return resultList
  
    if total is None:
      totalValues = [ safeSum(row) for row in izip_longest(*seriesList) ]
      totalText = "sumSeries(%s)" % formatPathExpressions(seriesList)
    elif type(total) is list:
      if len(total) != 1 and len(total) != len(seriesList):
        raise ValueError("asPercent second argument must be missing, a single digit, reference exactly 1 series or reference the same number of series as the first argument")
  
      if len(total) == 1:
        normalize([seriesList, total])
        totalValues = total[0]
        totalText = totalValues.name
    else:
      totalValues = [total] * len(seriesList[0])
      totalText = str(total)
  
    resultList = []
    if type(total) is list and len(total) == len(seriesList):
      for series1, series2 in matchSeries(seriesList, total):
        name = "asPercent(%s,%s)" % (series1.name,series2.name)
        (seriesList,start,end,step) = normalize([(series1, series2)])
        resultValues = [ safeMul(safeDiv(v1, v2), 100.0) for v1,v2 in izip_longest(series1,series2) ]
        resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
        resultList.append(resultSeries)
    else:
      for series in seriesList:
        resultValues = [ safeMul(safeDiv(val, totalVal), 100.0) for val,totalVal in izip_longest(series,totalValues) ]
  
        name = "asPercent(%s,%s)" % (series.name, totalText or series.pathExpression)
        resultSeries = TimeSeries(name,series.start,series.end,series.step,resultValues,xFilesFactor=xFilesFactor)
        resultList.append(resultSeries)
  
    return resultList
  
  
  asPercent.group = 'Combine'
  asPercent.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('total', ParamTypes.seriesList),
    Param('nodes', ParamTypes.nodeOrTag, multiple=True),
  ]
  
  
  def divideSeriesLists(requestContext, dividendSeriesList, divisorSeriesList):
    """
    Iterates over a two lists and divides list1[0] by list2[0], list1[1] by list2[1] and so on.
    The lists need to be the same length
    """
  
    if len(dividendSeriesList) != len(divisorSeriesList):
      raise ValueError("dividendSeriesList and divisorSeriesList argument must have equal length")
  
    results = []
  
    #for dividendSeries in dividendSeriesList:
    for i in range(0, len(dividendSeriesList)):
      dividendSeries = dividendSeriesList[i]
      divisorSeries = divisorSeriesList[i]
  
      name = "divideSeries(%s,%s)" % (dividendSeries.name, divisorSeries.name)
      bothSeries = (dividendSeries, divisorSeries)
      step = reduce(lcm,[s.step for s in bothSeries])
  
      for s in bothSeries:
        s.consolidate( step // s.step )
  
      start = min([s.start for s in bothSeries])
      end = max([s.end for s in bothSeries])
      end -= (end - start) % step
  
      values = ( safeDiv(v1,v2) for v1,v2 in izip_longest(*bothSeries) )
  
      quotientSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
      results.append(quotientSeries)
  
    return results
  
  
  divideSeriesLists.group = 'Combine'
  divideSeriesLists.params = [
    Param('dividendSeriesList', ParamTypes.seriesList, required=True),
    Param('divisorSeriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def divideSeries(requestContext, dividendSeriesList, divisorSeries):
    """
    Takes a dividend metric and a divisor metric and draws the division result.
    A constant may *not* be passed. To divide by a constant, use the scale()
    function (which is essentially a multiplication operation) and use the inverse
    of the dividend. (Division by 8 = multiplication by 1/8 or 0.125)
  
    Example:
  
    .. code-block:: none
  
      &target=divideSeries(Series.dividends,Series.divisors)
  
  
    """
    if len(divisorSeries) == 0:
      for series in dividendSeriesList:
        series.name = "divideSeries(%s,MISSING)" % series.name
        series.pathExpression = series.name
        for i in range(len(series)):
          series[i] = None
      return dividendSeriesList
    if len(divisorSeries) > 1:
      raise ValueError("divideSeries second argument must reference exactly 1 series (got {0})".format(len(divisorSeries)))
  
    divisorSeries = divisorSeries[0]
    results = []
  
    for dividendSeries in dividendSeriesList:
      name = "divideSeries(%s,%s)" % (dividendSeries.name, divisorSeries.name)
      bothSeries = (dividendSeries, divisorSeries)
      step = reduce(lcm,[s.step for s in bothSeries])
  
      for s in bothSeries:
        s.consolidate( step // s.step )
  
      start = min([s.start for s in bothSeries])
      end = max([s.end for s in bothSeries])
      end -= (end - start) % step
  
      values = ( safeDiv(v1,v2) for v1,v2 in izip_longest(*bothSeries) )
  
      quotientSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
      results.append(quotientSeries)
  
    return results
  
  
  divideSeries.group = 'Combine'
  divideSeries.params = [
    Param('dividendSeriesList', ParamTypes.seriesList, required=True),
    Param('divisorSeries', ParamTypes.seriesList, required=True),
  ]
  
  
  def multiplySeries(requestContext, *seriesLists):
    """
    Takes two or more series and multiplies their points. A constant may not be
    used. To multiply by a constant, use the scale() function.
  
    Example:
  
    .. code-block:: none
  
      &target=multiplySeries(Series.dividends,Series.divisors)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``multiply``.
    """
    # special handling for legacy multiplySeries behavior with a single series
    if len(seriesLists) == 1 and len(seriesLists[0]) == 1:
      return seriesLists[0]
  
    return aggregate(requestContext, seriesLists, 'multiply')
  
  
  multiplySeries.group = 'Combine'
  multiplySeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def weightedAverage(requestContext, seriesListAvg, seriesListWeight, *nodes):
    """
    Takes a series of average values and a series of weights and
    produces a weighted average for all values.
    The corresponding values should share one or more zero-indexed nodes and/or tags.
  
    Example:
  
    .. code-block:: none
  
      &target=weightedAverage(*.transactions.mean,*.transactions.count,0)
  
    Each node may be an integer referencing a node in the series name or a string identifying a tag.
    """
    sortedSeries={}
  
    for seriesAvg, seriesWeight in izip_longest(seriesListAvg , seriesListWeight):
      key = aggKey(seriesAvg, nodes)
  
      if key not in sortedSeries:
        sortedSeries[key]={}
      sortedSeries[key]['avg']=seriesAvg
  
      key = aggKey(seriesWeight, nodes)
  
      if key not in sortedSeries:
        sortedSeries[key]={}
      sortedSeries[key]['weight']=seriesWeight
  
    productList = []
  
    for key in sortedSeries.keys():
      if 'weight' not in sortedSeries[key]:
        continue
      if 'avg' not in sortedSeries[key]:
        continue
  
      seriesWeight = sortedSeries[key]['weight']
      seriesAvg = sortedSeries[key]['avg']
  
      productValues = [ safeMul(val1, val2) for val1,val2 in izip_longest(seriesAvg,seriesWeight) ]
      name='product(%s,%s)' % (seriesWeight.name, seriesAvg.name)
      productSeries = TimeSeries(name,seriesAvg.start,seriesAvg.end,seriesAvg.step,productValues,xFilesFactor=requestContext.get('xFilesFactor'))
      productList.append(productSeries)
  
    if not productList:
      return []
  
    sumProducts=sumSeries(requestContext, productList)[0]
    sumWeights=sumSeries(requestContext, seriesListWeight)[0]
  
    resultValues = [ safeDiv(val1, val2) for val1,val2 in izip_longest(sumProducts,sumWeights) ]
    name = "weightedAverage(%s, %s, %s)" % (','.join(sorted(set(s.pathExpression for s in seriesListAvg))) ,','.join(sorted(set(s.pathExpression for s in seriesListWeight))), ','.join(map(str,nodes)))
    resultSeries = TimeSeries(name,sumProducts.start,sumProducts.end,sumProducts.step,resultValues,xFilesFactor=requestContext.get('xFilesFactor'))
    return [resultSeries]
  
  
  weightedAverage.group = 'Combine'
  weightedAverage.params = [
    Param('seriesListAvg', ParamTypes.seriesList, required=True),
    Param('seriesListWeight', ParamTypes.seriesList, required=True),
    Param('nodes', ParamTypes.nodeOrTag, multiple=True),
  ]
  
  intOrIntervalSuggestions = [5, 7, 10, '1min', '5min', '10min', '30min', '1hour']
  
  
  def movingWindow(requestContext, seriesList, windowSize, func='average', xFilesFactor=None):
    """
    Graphs a moving window function of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList, a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), a function to apply to the points
    in the window to produce the output, and an xFilesFactor value to specify how many points in the
    window must be non-null for the output to be considered valid. Graphs the
    output of the function for the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingWindow(Server.instance01.threads.busy,10)
      &target=movingWindow(Server.instance*.threads.idle,'5min','median',0.5)
  
    .. note::
  
      `xFilesFactor` follows the same semantics as in Whisper storage schemas.  Setting it to 0 (the
      default) means that only a single value in a given interval needs to be non-null, setting it to
      1 means that all values in the interval must be non-null.  A setting of 0.5 means that at least
      half the values in the interval must be non-null.
    """
    if not seriesList:
      return []
  
    if isinstance(windowSize, six.string_types):
      delta = parseTimeOffset(windowSize)
      previewSeconds = abs(delta.seconds + (delta.days * 86400))
    else:
      previewSeconds = max([s.step for s in seriesList]) * int(windowSize)
  
    consolidateFunc = getAggFunc(func)
  
    # ignore original data and pull new, including our preview
    # data from earlier is needed to calculate the early results
    newContext = requestContext.copy()
    newContext['startTime'] = requestContext['startTime'] -  timedelta(seconds=previewSeconds)
    previewList = evaluateTarget(newContext, requestContext['args'][0])
    result = []
  
    tagName = 'moving' + func.capitalize()
  
    for series in previewList:
      if isinstance(windowSize, six.string_types):
        newName = '%s(%s,"%s")' % (tagName, series.name, windowSize)
        windowPoints = previewSeconds // series.step
      else:
        newName = '%s(%s,%s)' % (tagName, series.name, windowSize)
        windowPoints = int(windowSize)
  
      series.tags[tagName] = windowSize
      newSeries = series.copy(name=newName, start=series.start + previewSeconds, values=[])
  
      effectiveXFF = xFilesFactor if xFilesFactor is not None else series.xFilesFactor
  
      for i in range(windowPoints, len(series)):
        nonNull = [v for v in series[i - windowPoints:i] if v is not None]
  
        if nonNull and xff(len(nonNull), windowPoints, effectiveXFF):
          val = consolidateFunc(nonNull)
        else:
          val = None
        newSeries.append(val)
  
      result.append(newSeries)
  
    return result
  
  
  movingWindow.group = 'Calculate'
  movingWindow.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('func', ParamTypes.string, options=aggFuncNames, default='average'),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def exponentialMovingAverage(requestContext, seriesList, windowSize):
    """
    Takes a series of values and a window size and produces an exponential moving
    average utilizing the following formula:
  
    .. code-block:: none
  
      ema(current) = constant * (Current Value) + (1 - constant) * ema(previous)
  
    The Constant is calculated as:
  
    .. code-block:: none
  
      constant = 2 / (windowSize + 1)
  
    The first period EMA uses a simple moving average for its value.
  
    Example:
  
    .. code-block:: none
  
      &target=exponentialMovingAverage(*.transactions.count, 10)
      &target=exponentialMovingAverage(*.transactions.count, '-10s')
  
    """
    # EMA = C * (current_value) + (1 - C) + EMA
    # C = 2 / (windowSize + 1)
  
    # The following was copied from movingAverage, and altered for ema
    if not seriesList:
      return []
    # set previewSeconds and constant based on windowSize string or integer
    if isinstance(windowSize, six.string_types):
      delta = parseTimeOffset(windowSize)
      previewSeconds = abs(delta.seconds + (delta.days * 86400))
      constant = (float(2) / (int(previewSeconds) + 1))
    else:
      previewSeconds = max([s.step for s in seriesList]) * int(windowSize)
      constant = (float(2) / (int(windowSize) + 1))
  
    # ignore original data and pull new, including our preview
    # data from earlier is needed to calculate the early results
    newContext = requestContext.copy()
    newContext['startTime'] = requestContext['startTime'] -  timedelta(seconds=previewSeconds)
    previewList = evaluateTarget(newContext, requestContext['args'][0])
    result = []
  
    for series in previewList:
      if isinstance(windowSize, six.string_types):
        newName = 'exponentialMovingAverage(%s,"%s")' % (series.name, windowSize)
        windowPoints = previewSeconds // series.step
      else:
        newName = "exponentialMovingAverage(%s,%s)" % (series.name, windowSize)
        windowPoints = int(windowSize)
  
      series.tags['exponentialMovingAverage'] = windowSize
      newSeries = series.copy(name=newName, start=series.start + previewSeconds, values=[])
  
      ema = safeAvg(series[:windowPoints]) or 0
      newSeries.append(round(ema, 6))
  
      for i in range(windowPoints, len(series)):
        if series[i] is not None:
          ema = constant * series[i] + (1 - constant) * ema
          newSeries.append(round(ema, 6))
        else:
          newSeries.append(None)
  
      result.append(newSeries)
  
    return result
  
  
  exponentialMovingAverage.group = 'Calculate'
  exponentialMovingAverage.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
  ]
  
  
  def movingMedian(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving median of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    median of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMedian(Server.instance01.threads.busy,10)
      &target=movingMedian(Server.instance*.threads.idle,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'median', xFilesFactor)
  
  
  movingMedian.group = 'Calculate'
  movingMedian.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def scale(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and multiplies the datapoint
    by the constant provided at each point.
  
    Example:
  
    .. code-block:: none
  
      &target=scale(Server.instance01.threads.busy,10)
      &target=scale(Server.instance*.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['scale'] = factor
      series.name = "scale(%s,%g)" % (series.name,float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safeMul(value,factor)
    return seriesList
  
  
  scale.group = 'Transform'
  scale.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def scaleToSeconds(requestContext, seriesList, seconds):
    """
    Takes one metric or a wildcard seriesList and returns "value per seconds" where
    seconds is a last argument to this functions.
  
    Useful in conjunction with derivative or integral function if you want
    to normalize its result to a known resolution for arbitrary retentions
    """
  
    for series in seriesList:
      series.tags['scaleToSeconds'] = seconds
      series.name = "scaleToSeconds(%s,%d)" % (series.name,seconds)
      series.pathExpression = series.name
      factor = seconds / series.step
  
      for i,value in enumerate(series):
        if value is not None:
          # Division long by float cause OverflowError
          try:
            series[i] = round(value * factor, 6)
          except OverflowError:
            series[i] = (value // series.step) * seconds
  
    return seriesList
  
  
  scaleToSeconds.group = 'Transform'
  scaleToSeconds.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('seconds', ParamTypes.integer, required=True),
  ]
  
  
  def pow(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and raises the datapoint
    by the power of the constant provided at each point.
  
    Example:
  
    .. code-block:: none
  
      &target=pow(Server.instance01.threads.busy,10)
      &target=pow(Server.instance*.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['pow'] = factor
      series.name = "pow(%s,%g)" % (series.name, float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safePow(value, factor)
    return seriesList
  
  
  pow.group = 'Transform'
  pow.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def powSeries(requestContext, *seriesLists):
    """
    Takes two or more series and pows their points. A constant line may be
    used.
  
    Example:
  
    .. code-block:: none
  
      &target=powSeries(Server.instance01.app.requests, Server.instance01.app.replies)
  
  
    """
  
    try:
      (seriesList, start, end, step) = normalize(seriesLists)
    except NormalizeEmptyResultError:
      return []
    name = "powSeries(%s)" % ','.join([s.name for s in seriesList])
    values = []
    for row in izip_longest(*seriesList):
      first = True
      tmpVal = None
      for element in row:
        # If it is a first iteration - tmpVal needs to be element
        if first:
          tmpVal = element
          first = False
        else:
          tmpVal = safePow(tmpVal, element)
      values.append(tmpVal)
    series = TimeSeries(name,start,end,step,values,xFilesFactor=requestContext.get('xFilesFactor'))
    return [series]
  
  
  powSeries.group = 'Transform'
  powSeries.params = [
    Param('seriesList', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def squareRoot(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList, and computes the square root of each datapoint.
  
    Example:
  
    .. code-block:: none
  
      &target=squareRoot(Server.instance01.threads.busy)
  
    """
    for series in seriesList:
      series.tags['squareRoot'] = 1
      series.name = "squareRoot(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safePow(value, 0.5)
    return seriesList
  
  
  squareRoot.group = 'Transform'
  squareRoot.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def invert(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList, and inverts each datapoint (i.e. 1/x).
  
    Example:
  
    .. code-block:: none
  
      &target=invert(Server.instance01.threads.busy)
  
    """
    for series in seriesList:
      series.tags['invert'] = 1
      series.name = "invert(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
          series[i] = safePow(value, -1)
    return seriesList
  
  
  invert.group = 'Transform'
  invert.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def absolute(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList and applies the mathematical abs function to each
    datapoint transforming it to its absolute value.
  
    Example:
  
    .. code-block:: none
  
      &target=absolute(Server.instance01.threads.busy)
      &target=absolute(Server.instance*.threads.busy)
    """
    for series in seriesList:
      series.tags['absolute'] = 1
      series.name = "absolute(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safeAbs(value)
    return seriesList
  
  
  absolute.group = 'Transform'
  absolute.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def offset(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and adds the constant to
    each datapoint.
  
    Example:
  
    .. code-block:: none
  
      &target=offset(Server.instance01.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['offset'] = factor
      series.name = "offset(%s,%g)" % (series.name,float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = value + factor
    return seriesList
  
  
  offset.group = 'Transform'
  offset.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def offsetToZero(requestContext, seriesList):
    """
    Offsets a metric or wildcard seriesList by subtracting the minimum
    value in the series from each datapoint.
  
    Useful to compare different series where the values in each series
    may be higher or lower on average but you're only interested in the
    relative difference.
  
    An example use case is for comparing different round trip time
    results. When measuring RTT (like pinging a server), different
    devices may come back with consistently different results due to
    network latency which will be different depending on how many
    network hops between the probe and the device. To compare different
    devices in the same graph, the network latency to each has to be
    factored out of the results. This is a shortcut that takes the
    fastest response (lowest number in the series) and sets that to zero
    and then offsets all of the other datapoints in that series by that
    amount. This makes the assumption that the lowest response is the
    fastest the device can respond, of course the more datapoints that
    are in the series the more accurate this assumption is.
  
    Example:
  
    .. code-block:: none
  
      &target=offsetToZero(Server.instance01.responseTime)
      &target=offsetToZero(Server.instance*.responseTime)
  
    """
    for series in seriesList:
      minimum = safeMin(series)
      series.tags['offsetToZero'] = minimum
      series.name = "offsetToZero(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = value - minimum
    return seriesList
  
  
  offsetToZero.group = 'Transform'
  offsetToZero.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def roundFunction(requestContext, seriesList, precision=None):
    """
    Takes one metric or a wildcard seriesList optionally followed by a precision, and rounds each
    datapoint to the specified precision.
  
    Example:
  
    .. code-block:: none
  
      &target=round(Server.instance01.threads.busy)
      &target=round(Server.instance01.threads.busy,2)
  
    """
    for series in seriesList:
      series.tags['round'] = precision or 0
      if precision is None:
        series.name = "round(%s)" % (series.name)
      else:
        series.name = "round(%s,%g)" % (series.name,int(precision))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = round(value, precision or 0)
    return seriesList
  
  
  roundFunction.group = 'Transform'
  roundFunction.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('precision', ParamTypes.integer, default=0),
  ]
  
  
  def movingAverage(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving average of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    average of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingAverage(Server.instance01.threads.busy,10)
      &target=movingAverage(Server.instance*.threads.idle,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'average', xFilesFactor)
  
  
  movingAverage.group = 'Calculate'
  movingAverage.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingSum(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving sum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    sum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingSum(Server.instance01.requests,10)
      &target=movingSum(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'sum', xFilesFactor)
  
  
  movingSum.group = 'Calculate'
  movingSum.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingMin(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving minimum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    minimum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMin(Server.instance01.requests,10)
      &target=movingMin(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'min', xFilesFactor)
  
  
  movingMin.group = 'Calculate'
  movingMin.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingMax(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving maximum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    maximum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMax(Server.instance01.requests,10)
      &target=movingMax(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'max', xFilesFactor)
  
  
  movingMax.group = 'Calculate'
  movingMax.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def cumulative(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList.
  
    When a graph is drawn where width of the graph size in pixels is smaller than
    the number of datapoints to be graphed, Graphite consolidates the values to
    to prevent line overlap. The cumulative() function changes the consolidation
    function from the default of 'average' to 'sum'. This is especially useful in
    sales graphs, where fractional values make no sense and a 'sum' of consolidated
    values is appropriate.
  
    Alias for :func:`consolidateBy(series, 'sum') <graphite.render.functions.consolidateBy>`
  
    .. code-block:: none
  
      &target=cumulative(Sales.widgets.largeBlue)
  
    """
    return consolidateBy(requestContext, seriesList, 'sum')
  
  
  cumulative.group = 'Special'
  cumulative.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def consolidateBy(requestContext, seriesList, consolidationFunc):
    """
    Takes one metric or a wildcard seriesList and a consolidation function name.
  
    Valid function names are 'sum', 'average', 'min', 'max', 'first' & 'last'.
  
    When a graph is drawn where width of the graph size in pixels is smaller than
    the number of datapoints to be graphed, Graphite consolidates the values to
    to prevent line overlap. The consolidateBy() function changes the consolidation
    function from the default of 'average' to one of 'sum', 'max', 'min', 'first', or 'last'.
    This is especially useful in sales graphs, where fractional values make no sense and a 'sum'
    of consolidated values is appropriate.
  
    .. code-block:: none
  
      &target=consolidateBy(Sales.widgets.largeBlue, 'sum')
      &target=consolidateBy(Servers.web01.sda1.free_space, 'max')
  
    """
    for series in seriesList:
      # datalib will throw an exception, so it's not necessary to validate here
      series.consolidationFunc = consolidationFunc
      series.tags['consolidateBy'] = consolidationFunc
      series.name = 'consolidateBy(%s,"%s")' % (series.name, series.consolidationFunc)
      series.pathExpression = series.name
    return seriesList
  
  
  consolidateBy.group = 'Special'
  consolidateBy.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('consolidationFunc', ParamTypes.string, required=True, options=['sum', 'average', 'min', 'max', 'first', 'last']),
  ]
  
  
  def setXFilesFactor(requestContext, seriesList, xFilesFactor):
    """
    Short form: xFilesFactor()
  
    Takes one metric or a wildcard seriesList and an xFilesFactor value between 0 and 1
  
    When a series needs to be consolidated, this sets the fraction of values in an interval that must
    not be null for the consolidation to be considered valid.  If there are not enough values then
    None will be returned for that interval.
  
    .. code-block:: none
  
      &target=xFilesFactor(Sales.widgets.largeBlue, 0.5)
      &target=Servers.web01.sda1.free_space|consolidateBy('max')|xFilesFactor(0.5)
  
    The `xFilesFactor` set via this function is used as the default for all functions that accept an
    `xFilesFactor` parameter, all functions that aggregate data across multiple series and/or
    intervals, and `maxDataPoints <render_api.html#maxdatapoints>`_ consolidation.
  
    A default for the entire render request can also be set using the
    `xFilesFactor <render_api.html#xfilesfactor>`_ query parameter.
  
    .. note::
  
      `xFilesFactor` follows the same semantics as in Whisper storage schemas.  Setting it to 0 (the
      default) means that only a single value in a given interval needs to be non-null, setting it to
      1 means that all values in the interval must be non-null.  A setting of 0.5 means that at least
      half
  
  [... the rest is omitted ...]
files.new_path: webapp/graphite/render/functions.py
files.old_contents: #Copyright 2008 Orbitz WorldWide
  #
  #Licensed under the Apache License, Version 2.0 (the "License");
  #you may not use this file except in compliance with the License.
  #You may obtain a copy of the License at
  #
  #   http://www.apache.org/licenses/LICENSE-2.0
  #
  #Unless required by applicable law or agreed to in writing, software
  #distributed under the License is distributed on an "AS IS" BASIS,
  #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  #See the License for the specific language governing permissions and
  #limitations under the License.
  
  # make / work consistently between python 2.x and 3.x
  # https://www.python.org/dev/peps/pep-0238/
  from __future__ import division
  
  import math
  import random
  import re
  import time
  
  from datetime import datetime, timedelta
  from functools import reduce
  from six.moves import range, zip
  import six
  try:
    from itertools import izip, izip_longest
  except ImportError:
    # Python 3
    from itertools import zip_longest as izip_longest
    izip = zip
  from os import environ
  
  from django.conf import settings
  
  from graphite.errors import NormalizeEmptyResultError
  from graphite.events import models
  from graphite.functions import SeriesFunction, ParamTypes, Param
  from graphite.logger import log
  from graphite.render.attime import getUnitString, parseTimeOffset, parseATTime, SECONDS_STRING, MINUTES_STRING, HOURS_STRING, DAYS_STRING, WEEKS_STRING, MONTHS_STRING, YEARS_STRING
  from graphite.render.evaluator import evaluateTarget
  from graphite.render.grammar import grammar
  from graphite.storage import STORE
  from graphite.util import epoch, epoch_to_dt, timestamp, deltaseconds
  
  # XXX format_units() should go somewhere else
  if environ.get('READTHEDOCS'):
    format_units = lambda *args, **kwargs: (0,'')
  else:
    from graphite.render.glyph import format_units
    from graphite.render.datalib import TimeSeries
  
  NAN = float('NaN')
  INF = float('inf')
  DAY = 86400
  HOUR = 3600
  MINUTE = 60
  
  #Utility functions
  
  
  def safeSum(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return sum(safeValues)
  
  
  def safeDiff(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      values = list(map(lambda x: x*-1, safeValues[1:]))
      values.insert(0, safeValues[0])
      return sum(values)
  
  
  def safeLen(values):
    return len([v for v in values if v is not None])
  
  
  def safeDiv(a, b):
    if a is None: return None
    if b in (0,None): return None
    return a / b
  
  
  def safePow(a, b):
    if a is None: return None
    if b is None: return None
    try:
      result = math.pow(a, b)
    except (ValueError, OverflowError):
      return None
    return result
  
  
  def safeMul(*factors):
    if None in factors:
      return None
  
    factors = [float(x) for x in factors]
    product = reduce(lambda x,y: x*y, factors)
    return product
  
  
  def safeSubtract(a,b):
      if a is None or b is None: return None
      return float(a) - float(b)
  
  
  def safeAvg(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return sum(safeValues) / len(safeValues)
  
  
  def safeMedian(values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      sortedVals = sorted(safeValues)
      mid = len(sortedVals) // 2
      if len(sortedVals) % 2 == 0:
        return float(sortedVals[mid-1] + sortedVals[mid]) / 2
      else:
        return sortedVals[mid]
  
  
  def safeStdDev(a):
    sm = safeSum(a)
    ln = safeLen(a)
    avg = safeDiv(sm,ln)
    if avg is None: return None
    sum = 0
    safeValues = [v for v in a if v is not None]
    for val in safeValues:
       sum = sum + (val - avg) * (val - avg)
    return math.sqrt(sum/ln)
  
  
  def safeLast(values):
    for v in reversed(values):
      if v is not None: return v
  
  
  def safeMin(values, default=None):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return min(safeValues)
    else:
      return default
  
  
  def safeMax(values, default=None):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return max(safeValues)
    else:
      return default
  
  
  def safeMap(function, values):
    safeValues = [v for v in values if v is not None]
    if safeValues:
      return [function(x) for x in safeValues]
  
  
  def safeAbs(value):
    if value is None: return None
    return abs(value)
  
  
  # In Py2, None < (any integer) . Use -inf for same sorting on Python 3
  def keyFunc(func):
    def safeFunc(values):
      val = func(values)
      return val if val is not None else -INF
    return safeFunc
  
  
  # Greatest common divisor
  def gcd(a, b):
    if b == 0:
      return a
    return gcd(b, a%b)
  
  
  # Least common multiple
  def lcm(a, b):
    if a == b: return a
    if a < b: (a, b) = (b, a) #ensure a > b
    return a // gcd(a,b) * b
  
  
  # check list of values against xFilesFactor
  def xffValues(values, xFilesFactor):
    if not values:
      return False
    return xff(len([v for v in values if v is not None]), len(values), xFilesFactor)
  
  
  def xff(nonNull, total, xFilesFactor=None):
    if not nonNull or not total:
      return False
    return nonNull / total >= (xFilesFactor if xFilesFactor is not None else settings.DEFAULT_XFILES_FACTOR)
  
  
  def getNodeOrTag(series, n, pathExpression=None):
    try:
      # if n is an integer use it as a node, otherwise catch ValueError and treat it as a tag
      if n == int(n):
        # first split off any tags, then list of nodes is name split on .
        return (pathExpression or series.name).split(';', 2)[0].split('.')[n]
    except (ValueError, IndexError):
      # if node isn't an integer or isn't found then try it as a tag name
      pass
    # return tag value, default to '' if not found
    return series.tags.get(str(n), '')
  
  
  def aggKey(series, nodes, pathExpression=None):
    # if series.name looks like it includes a function, use the first path expression
    if pathExpression is None and series.name[-1] == ')':
      pathExpression = _getFirstPathExpression(series.name)
    return '.'.join([getNodeOrTag(series, n, pathExpression) for n in nodes])
  
  
  def normalize(seriesLists):
    if seriesLists:
      seriesList = reduce(lambda L1,L2: L1+L2,seriesLists)
      if seriesList:
        step = reduce(lcm,[s.step for s in seriesList])
        for s in seriesList:
          s.consolidate( step // s.step )
        start = min([s.start for s in seriesList])
        end = max([s.end for s in seriesList])
        end -= (end - start) % step
        return (seriesList,start,end,step)
    raise NormalizeEmptyResultError()
  
  
  def matchSeries(seriesList1, seriesList2):
    assert len(seriesList2) == len(seriesList1), "The number of series in each argument must be the same"
    return izip(sorted(seriesList1, key=lambda a: a.name), sorted(seriesList2, key=lambda a: a.name))
  
  
  def formatPathExpressions(seriesList):
    # remove duplicates
    pathExpressions = []
    [pathExpressions.append(s.pathExpression) for s in seriesList if not pathExpressions.count(s.pathExpression)]
    return ','.join(pathExpressions)
  
  
  # Series Functions
  
  aggFuncs = {
    'average': safeAvg,
    'median': safeMedian,
    'sum': safeSum,
    'min': safeMin,
    'max': safeMax,
    'diff': safeDiff,
    'stddev': safeStdDev,
    'count': safeLen,
    'range': lambda row: safeSubtract(safeMax(row), safeMin(row)),
    'multiply': lambda row: safeMul(*row),
    'last': safeLast,
  }
  
  aggFuncAliases = {
    'rangeOf': aggFuncs['range'],
    'avg': aggFuncs['average'],
    'total': aggFuncs['sum'],
    'current': aggFuncs['last'],
  }
  
  aggFuncNames = sorted(aggFuncs.keys())
  
  
  def getAggFunc(func, rawFunc=None):
    if func in aggFuncs:
      return aggFuncs[func]
    if func in aggFuncAliases:
      return aggFuncAliases[func]
    raise ValueError('Unsupported aggregation function: %s' % (rawFunc or func))
  
  
  def aggregate(requestContext, seriesList, func, xFilesFactor=None):
    """
    Aggregate series using the specified function.
  
    Example:
  
    .. code-block:: none
  
      &target=aggregate(host.cpu-[0-7].cpu-{user,system}.value, "sum")
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-{user,system}.value)
  
    This function can be used with aggregation functions ``average``, ``median``, ``sum``, ``min``,
    ``max``, ``diff``, ``stddev``, ``count``, ``range``, ``multiply`` & ``last``.
    """
    # strip Series from func if func was passed like sumSeries
    rawFunc = func
    if func[-6:] == 'Series':
      func = func[:-6]
  
    consolidationFunc = getAggFunc(func, rawFunc)
  
    # if seriesList is empty then just short-circuit
    if not seriesList:
      return []
  
    # if seriesList is a single series then wrap it for normalize
    if isinstance(seriesList[0], TimeSeries):
      seriesList = [seriesList]
  
    try:
      (seriesList, start, end, step) = normalize(seriesList)
    except NormalizeEmptyResultError:
      return []
    xFilesFactor = xFilesFactor if xFilesFactor is not None else requestContext.get('xFilesFactor')
    name = "%sSeries(%s)" % (func, formatPathExpressions(seriesList))
    values = ( consolidationFunc(row) if xffValues(row, xFilesFactor) else None for row in izip_longest(*seriesList) )
    tags = seriesList[0].tags
    for series in seriesList:
      tags = {tag: tags[tag] for tag in tags if tag in series.tags and tags[tag] == series.tags[tag]}
    if 'name' not in tags:
      tags['name'] = name
    tags['aggregatedBy'] = func
    series = TimeSeries(name, start, end, step, values, xFilesFactor=xFilesFactor, tags=tags)
  
    return [series]
  
  
  aggregate.group = 'Combine'
  aggregate.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('func', ParamTypes.aggFunc, required=True, options=aggFuncNames),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def sumSeries(requestContext, *seriesLists):
    """
    Short form: sum()
  
    This will add metrics together and return the sum at each datapoint. (See
    integral for a sum over time)
  
    Example:
  
    .. code-block:: none
  
      &target=sum(company.server.application*.requestsHandled)
  
    This would show the sum of all requests handled per minute (provided
    requestsHandled are collected once a minute).   If metrics with different
    retention rates are combined, the coarsest metric is graphed, and the sum
    of the other metrics is averaged for the metrics with finer retention rates.
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``sum``.
    """
    return aggregate(requestContext, seriesLists, 'sum')
  
  
  sumSeries.group = 'Combine'
  sumSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def sumSeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call sumSeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=sumSeriesWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-user.value)&target=sumSeries(host.cpu-[0-7].cpu-system.value)
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``sum``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'sum', *position)
  
  
  sumSeriesWithWildcards.group = 'Combine'
  sumSeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def averageSeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call averageSeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=averageSeriesWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=averageSeries(host.*.cpu-user.value)&target=averageSeries(host.*.cpu-system.value)
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``average``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'average', *position)
  
  
  averageSeriesWithWildcards.group = 'Combine'
  averageSeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def multiplySeriesWithWildcards(requestContext, seriesList, *position): #XXX
    """
    Call multiplySeries after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=multiplySeriesWithWildcards(web.host-[0-7].{avg-response,total-request}.value, 2)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=multiplySeries(web.host-0.{avg-response,total-request}.value)&target=multiplySeries(web.host-1.{avg-response,total-request}.value)...
  
    This is an alias for :py:func:`aggregateWithWildcards <aggregateWithWildcards>` with aggregation ``multiply``.
    """
    return aggregateWithWildcards(requestContext, seriesList, 'multiply', *position)
  
  
  multiplySeriesWithWildcards.group = 'Combine'
  multiplySeriesWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def aggregateWithWildcards(requestContext, seriesList, func, *positions):
    """
    Call aggregator after inserting wildcards at the given position(s).
  
    Example:
  
    .. code-block:: none
  
      &target=aggregateWithWildcards(host.cpu-[0-7].cpu-{user,system}.value, "sum", 1)
  
    This would be the equivalent of
  
    .. code-block:: none
  
      &target=sumSeries(host.cpu-[0-7].cpu-user.value)&target=sumSeries(host.cpu-[0-7].cpu-system.value)
      # or
      &target=aggregate(host.cpu-[0-7].cpu-user.value,"sum")&target=aggregate(host.cpu-[0-7].cpu-system.value,"sum")
  
    This function can be used with all aggregation functions supported by
    :py:func:`aggregate <aggregate>`: ``average``, ``median``, ``sum``, ``min``, ``max``, ``diff``,
    ``stddev``, ``range`` & ``multiply``.
  
    This complements :py:func:`groupByNodes <groupByNodes>` which takes a list of nodes that must match in each group.
    """
    metaSeries = {}
    keys = []
    for series in seriesList:
      key = '.'.join(map(lambda x: x[1], filter(lambda i: i[0] not in positions, enumerate(series.name.split('.')))))
      if key not in metaSeries:
        metaSeries[key] = [series]
        keys.append(key)
      else:
        metaSeries[key].append(series)
    for key in metaSeries.keys():
      metaSeries[key] = aggregate(requestContext, metaSeries[key], func)[0]
      metaSeries[key].name = key
    return [ metaSeries[key] for key in keys ]
  
  
  aggregateWithWildcards.group = 'Combine'
  aggregateWithWildcards.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('func', ParamTypes.aggFunc, required=True, options=aggFuncNames),
    Param('position', ParamTypes.node, multiple=True),
  ]
  
  
  def diffSeries(requestContext, *seriesLists):
    """
    Subtracts series 2 through n from series 1.
  
    Example:
  
    .. code-block:: none
  
      &target=diffSeries(service.connections.total,service.connections.failed)
  
    To diff a series and a constant, one should use offset instead of (or in
    addition to) diffSeries
  
    Example:
  
    .. code-block:: none
  
      &target=offset(service.connections.total,-5)
  
      &target=offset(diffSeries(service.connections.total,service.connections.failed),-4)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``diff``.
    """
    return aggregate(requestContext, seriesLists, 'diff')
  
  
  diffSeries.group = 'Combine'
  diffSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def averageSeries(requestContext, *seriesLists):
    """
    Short Alias: avg()
  
    Takes one metric or a wildcard seriesList.
    Draws the average value of all metrics passed at each time.
  
    Example:
  
    .. code-block:: none
  
      &target=averageSeries(company.server.*.threads.busy)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``average``.
    """
    return aggregate(requestContext, seriesLists, 'average')
  
  
  averageSeries.group = 'Combine'
  averageSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def stddevSeries(requestContext, *seriesLists):
    """
  
    Takes one metric or a wildcard seriesList.
    Draws the standard deviation of all metrics passed at each time.
  
    Example:
  
    .. code-block:: none
  
      &target=stddevSeries(company.server.*.threads.busy)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``stddev``.
    """
    return aggregate(requestContext, seriesLists, 'stddev')
  
  
  stddevSeries.group = 'Combine'
  stddevSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def minSeries(requestContext, *seriesLists):
    """
    Takes one metric or a wildcard seriesList.
    For each datapoint from each metric passed in, pick the minimum value and graph it.
  
    Example:
  
    .. code-block:: none
  
      &target=minSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``min``.
    """
    return aggregate(requestContext, seriesLists, 'min')
  
  
  minSeries.group = 'Combine'
  minSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def maxSeries(requestContext, *seriesLists):
    """
    Takes one metric or a wildcard seriesList.
    For each datapoint from each metric passed in, pick the maximum value and graph it.
  
    Example:
  
    .. code-block:: none
  
      &target=maxSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``max``.
    """
    return aggregate(requestContext, seriesLists, 'max')
  
  
  maxSeries.group = 'Combine'
  maxSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def rangeOfSeries(requestContext, *seriesLists):
    """
    Takes a wildcard seriesList.
    Distills down a set of inputs into the range of the series
  
    Example:
  
    .. code-block:: none
  
        &target=rangeOfSeries(Server*.connections.total)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``rangeOf``.
    """
    return aggregate(requestContext, seriesLists, 'rangeOf')
  
  
  rangeOfSeries.group = 'Combine'
  rangeOfSeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def percentileOfSeries(requestContext, seriesList, n, interpolate=False):
    """
    percentileOfSeries returns a single series which is composed of the n-percentile
    values taken across a wildcard series at each point. Unless `interpolate` is
    set to True, percentile values are actual values contained in one of the
    supplied series.
    """
    if n <= 0:
      raise ValueError('The requested percent is required to be greater than 0')
  
    # if seriesList is empty then just short-circuit
    if not seriesList:
      return []
  
    name = 'percentileOfSeries(%s,%g)' % (seriesList[0].pathExpression, n)
    (start, end, step) = normalize([seriesList])[1:]
    values = [ _getPercentile(row, n, interpolate) for row in izip_longest(*seriesList) ]
    resultSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
  
    return [resultSeries]
  
  
  percentileOfSeries.group = 'Combine'
  percentileOfSeries.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('n', ParamTypes.integer, required=True),
    Param('interpolate', ParamTypes.boolean, default=False),
  ]
  
  
  def keepLastValue(requestContext, seriesList, limit = INF):
    """
    Takes one metric or a wildcard seriesList, and optionally a limit to the number of 'None' values to skip over.
    Continues the line with the last received value when gaps ('None' values) appear in your data, rather than breaking your line.
  
    Example:
  
    .. code-block:: none
  
      &target=keepLastValue(Server01.connections.handled)
      &target=keepLastValue(Server01.connections.handled, 10)
  
    """
    for series in seriesList:
      series.name = "keepLastValue(%s)" % (series.name)
      series.pathExpression = series.name
      consecutiveNones = 0
      lastVal = None
      for i,value in enumerate(series):
        if value is None:
          consecutiveNones += 1
        else:
           if 0 < consecutiveNones <= limit and lastVal is not None:
             # If a non-None value is seen before the limit of Nones is hit,
             # backfill all the missing datapoints with the last known value.
             for index in range(i - consecutiveNones, i):
               series[index] = lastVal
  
           consecutiveNones = 0
           lastVal = value
  
      # If the series ends with some None values, try to backfill a bit to cover it.
      if 0 < consecutiveNones <= limit and lastVal is not None:
        for index in range(len(series) - consecutiveNones, len(series)):
          series[index] = lastVal
  
    return seriesList
  
  
  keepLastValue.group = 'Transform'
  keepLastValue.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('limit', ParamTypes.integer, default='INF'),
  ]
  
  
  def interpolate(requestContext, seriesList, limit = INF):
    """
    Takes one metric or a wildcard seriesList, and optionally a limit to the number of 'None' values to skip over.
    Continues the line with the last received value when gaps ('None' values) appear in your data, rather than breaking your line.
  
    Example:
  
    .. code-block:: none
  
      &target=interpolate(Server01.connections.handled)
      &target=interpolate(Server01.connections.handled, 10)
  
    """
    for series in seriesList:
      series.name = "interpolate(%s)" % (series.name)
      series.pathExpression = series.name
      consecutiveNones = 0
      for i,value in enumerate(series):
        series[i] = value
  
        # No 'keeping' can be done on the first value because we have no idea
        # what came before it.
        if i == 0:
          continue
  
        if value is None:
          consecutiveNones += 1
        elif consecutiveNones == 0: # have a value but no need to interpolate
          continue
        elif series[i - consecutiveNones - 1] is None: # have a value but can't interpolate: reset count
          consecutiveNones = 0
          continue
        else: # have a value and can interpolate
          # If a non-None value is seen before the limit of Nones is hit,
          # backfill all the missing datapoints with the last known value.
          if 0 < consecutiveNones <= limit:
            for index in range(i - consecutiveNones, i):
              series[index] = series[i - consecutiveNones - 1] + (index - (i - consecutiveNones -1)) * (value - series[i - consecutiveNones - 1]) / (consecutiveNones + 1)
  
          consecutiveNones = 0
  
      # If the series ends with some None values, try to backfill a bit to cover it.
      # if 0 < consecutiveNones < limit:
      #   for index in xrange(len(series) - consecutiveNones, len(series)):
      #     series[index] = series[len(series) - consecutiveNones - 1]
  
    return seriesList
  
  
  interpolate.group = 'Transform'
  interpolate.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('limit', ParamTypes.integer, default='INF'),
  ]
  
  
  def changed(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList.
    Output 1 when the value changed, 0 when null or the same
  
    Example:
  
    .. code-block:: none
  
      &target=changed(Server01.connections.handled)
  
    """
    for series in seriesList:
      series.name = "changed(%s)" % (series.name)
      series.pathExpression = series.name
      previous = None
      for i,value in enumerate(series):
        if previous is None:
          previous = value
          series[i] = 0
        elif value is not None and previous != value:
          series[i] = 1
          previous = value
        else:
          series[i] = 0
    return seriesList
  
  
  changed.group = 'Special'
  changed.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def asPercent(requestContext, seriesList, total=None, *nodes):
    """
  
    Calculates a percentage of the total of a wildcard series. If `total` is specified,
    each series will be calculated as a percentage of that total. If `total` is not specified,
    the sum of all points in the wildcard series will be used instead.
  
    A list of nodes can optionally be provided, if so they will be used to match series with their
    corresponding totals following the same logic as :py:func:`groupByNodes <groupByNodes>`.
  
    When passing `nodes` the `total` parameter may be a series list or `None`.  If it is `None` then
    for each series in `seriesList` the percentage of the sum of series in that group will be returned.
  
    When not passing `nodes`, the `total` parameter may be a single series, reference the same number
    of series as `seriesList` or be a numeric value.
  
    Example:
  
    .. code-block:: none
  
      # Server01 connections failed and succeeded as a percentage of Server01 connections attempted
      &target=asPercent(Server01.connections.{failed,succeeded}, Server01.connections.attempted)
  
      # For each server, its connections failed as a percentage of its connections attempted
      &target=asPercent(Server*.connections.failed, Server*.connections.attempted)
  
      # For each server, its connections failed and succeeded as a percentage of its connections attemped
      &target=asPercent(Server*.connections.{failed,succeeded}, Server*.connections.attempted, 0)
  
      # apache01.threads.busy as a percentage of 1500
      &target=asPercent(apache01.threads.busy,1500)
  
      # Server01 cpu stats as a percentage of its total
      &target=asPercent(Server01.cpu.*.jiffies)
  
      # cpu stats for each server as a percentage of its total
      &target=asPercent(Server*.cpu.*.jiffies, None, 0)
  
    When using `nodes`, any series or totals that can't be matched will create output series with
    names like ``asPercent(someSeries,MISSING)`` or ``asPercent(MISSING,someTotalSeries)`` and all
    values set to None. If desired these series can be filtered out by piping the result through
    ``|exclude("MISSING")`` as shown below:
  
    .. code-block:: none
  
      &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)
  
      # will produce 3 output series:
      # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected]
      # asPercent(Server2.memory.used,MISSING) [all values will be None]
      # asPercent(MISSING,Server3.memory.total) [all values will be None]
  
      &target=asPercent(Server{1,2}.memory.used,Server{1,3}.memory.total,0)|exclude("MISSING")
  
      # will produce 1 output series:
      # asPercent(Server1.memory.used,Server1.memory.total) [values will be as expected]
  
    Each node may be an integer referencing a node in the series name or a string identifying a tag.
  
    .. note::
  
      When `total` is a seriesList, specifying `nodes` to match series with the corresponding total
      series will increase reliability.
  
    """
    normalize([seriesList])
    xFilesFactor=requestContext.get('xFilesFactor')
  
    # if nodes are specified, use them to match series & total
    if nodes:
      keys = []
  
      # group series together by key
      metaSeries = {}
      for series in seriesList:
        key = aggKey(series, nodes)
        if key not in metaSeries:
          metaSeries[key] = [series]
          keys.append(key)
        else:
          metaSeries[key].append(series)
  
      # make list of totals
      totalSeries = {}
      # no total seriesList was specified, sum the values for each group of series
      if total is None:
        for key in keys:
          if len(metaSeries[key]) == 1:
            totalSeries[key] = metaSeries[key][0]
          else:
            name = 'sumSeries(%s)' % formatPathExpressions(metaSeries[key])
            (seriesList,start,end,step) = normalize([metaSeries[key]])
            totalValues = [ safeSum(row) for row in izip_longest(*metaSeries[key]) ]
            totalSeries[key] = TimeSeries(name,start,end,step,totalValues,xFilesFactor=xFilesFactor)
      # total seriesList was specified, sum the values for each group of totals
      elif isinstance(total, list):
        for series in total:
          key = aggKey(series, nodes)
          if key not in totalSeries:
            totalSeries[key] = [series]
            if key not in keys:
              keys.append(key)
          else:
            totalSeries[key].append(series)
  
        for key in totalSeries.keys():
          if len(totalSeries[key]) == 1:
            totalSeries[key] = totalSeries[key][0]
          else:
            name = 'sumSeries(%s)' % formatPathExpressions(totalSeries[key])
            (seriesList,start,end,step) = normalize([totalSeries[key]])
            totalValues = [ safeSum(row) for row in izip_longest(*totalSeries[key]) ]
            totalSeries[key] = TimeSeries(name,start,end,step,totalValues,xFilesFactor=xFilesFactor)
      # trying to use nodes with a total value, which isn't supported because it has no effect
      else:
        raise ValueError('total must be None or a seriesList')
  
      resultList = []
      for key in keys:
        # no series, must have total only
        if key not in metaSeries:
          series2 = totalSeries[key]
          name = "asPercent(%s,%s)" % ('MISSING', series2.name)
          resultValues = [ None for v1 in series2 ]
          resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
          resultList.append(resultSeries)
          continue
  
        for series1 in metaSeries[key]:
          # no total
          if key not in totalSeries:
            name = "asPercent(%s,%s)" % (series1.name, 'MISSING')
            resultValues = [ None for v1 in series1 ]
          # series and total
          else:
            series2 = totalSeries[key]
            name = "asPercent(%s,%s)" % (series1.name, series2.name)
            (seriesList,start,end,step) = normalize([(series1, series2)])
            resultValues = [ safeMul(safeDiv(v1, v2), 100.0) for v1,v2 in izip_longest(series1,series2) ]
  
          resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
          resultList.append(resultSeries)
  
      return resultList
  
    if total is None:
      totalValues = [ safeSum(row) for row in izip_longest(*seriesList) ]
      totalText = "sumSeries(%s)" % formatPathExpressions(seriesList)
    elif type(total) is list:
      if len(total) != 1 and len(total) != len(seriesList):
        raise ValueError("asPercent second argument must be missing, a single digit, reference exactly 1 series or reference the same number of series as the first argument")
  
      if len(total) == 1:
        normalize([seriesList, total])
        totalValues = total[0]
        totalText = totalValues.name
    else:
      totalValues = [total] * len(seriesList[0])
      totalText = str(total)
  
    resultList = []
    if type(total) is list and len(total) == len(seriesList):
      for series1, series2 in matchSeries(seriesList, total):
        name = "asPercent(%s,%s)" % (series1.name,series2.name)
        (seriesList,start,end,step) = normalize([(series1, series2)])
        resultValues = [ safeMul(safeDiv(v1, v2), 100.0) for v1,v2 in izip_longest(series1,series2) ]
        resultSeries = TimeSeries(name,start,end,step,resultValues,xFilesFactor=xFilesFactor)
        resultList.append(resultSeries)
    else:
      for series in seriesList:
        resultValues = [ safeMul(safeDiv(val, totalVal), 100.0) for val,totalVal in izip_longest(series,totalValues) ]
  
        name = "asPercent(%s,%s)" % (series.name, totalText or series.pathExpression)
        resultSeries = TimeSeries(name,series.start,series.end,series.step,resultValues,xFilesFactor=xFilesFactor)
        resultList.append(resultSeries)
  
    return resultList
  
  
  asPercent.group = 'Combine'
  asPercent.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('total', ParamTypes.seriesList),
    Param('nodes', ParamTypes.nodeOrTag, multiple=True),
  ]
  
  
  def divideSeriesLists(requestContext, dividendSeriesList, divisorSeriesList):
    """
    Iterates over a two lists and divides list1[0] by list2[0], list1[1] by list2[1] and so on.
    The lists need to be the same length
    """
  
    if len(dividendSeriesList) != len(divisorSeriesList):
      raise ValueError("dividendSeriesList and divisorSeriesList argument must have equal length")
  
    results = []
  
    #for dividendSeries in dividendSeriesList:
    for i in range(0, len(dividendSeriesList)):
      dividendSeries = dividendSeriesList[i]
      divisorSeries = divisorSeriesList[i]
  
      name = "divideSeries(%s,%s)" % (dividendSeries.name, divisorSeries.name)
      bothSeries = (dividendSeries, divisorSeries)
      step = reduce(lcm,[s.step for s in bothSeries])
  
      for s in bothSeries:
        s.consolidate( step // s.step )
  
      start = min([s.start for s in bothSeries])
      end = max([s.end for s in bothSeries])
      end -= (end - start) % step
  
      values = ( safeDiv(v1,v2) for v1,v2 in izip_longest(*bothSeries) )
  
      quotientSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
      results.append(quotientSeries)
  
    return results
  
  
  divideSeriesLists.group = 'Combine'
  divideSeriesLists.params = [
    Param('dividendSeriesList', ParamTypes.seriesList, required=True),
    Param('divisorSeriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def divideSeries(requestContext, dividendSeriesList, divisorSeries):
    """
    Takes a dividend metric and a divisor metric and draws the division result.
    A constant may *not* be passed. To divide by a constant, use the scale()
    function (which is essentially a multiplication operation) and use the inverse
    of the dividend. (Division by 8 = multiplication by 1/8 or 0.125)
  
    Example:
  
    .. code-block:: none
  
      &target=divideSeries(Series.dividends,Series.divisors)
  
  
    """
    if len(divisorSeries) == 0:
      for series in dividendSeriesList:
        series.name = "divideSeries(%s,MISSING)" % series.name
        series.pathExpression = series.name
        for i in range(len(series)):
          series[i] = None
      return dividendSeriesList
    if len(divisorSeries) > 1:
      raise ValueError("divideSeries second argument must reference exactly 1 series (got {0})".format(len(divisorSeries)))
  
    divisorSeries = divisorSeries[0]
    results = []
  
    for dividendSeries in dividendSeriesList:
      name = "divideSeries(%s,%s)" % (dividendSeries.name, divisorSeries.name)
      bothSeries = (dividendSeries, divisorSeries)
      step = reduce(lcm,[s.step for s in bothSeries])
  
      for s in bothSeries:
        s.consolidate( step // s.step )
  
      start = min([s.start for s in bothSeries])
      end = max([s.end for s in bothSeries])
      end -= (end - start) % step
  
      values = ( safeDiv(v1,v2) for v1,v2 in izip_longest(*bothSeries) )
  
      quotientSeries = TimeSeries(name, start, end, step, values, xFilesFactor=requestContext.get('xFilesFactor'))
      results.append(quotientSeries)
  
    return results
  
  
  divideSeries.group = 'Combine'
  divideSeries.params = [
    Param('dividendSeriesList', ParamTypes.seriesList, required=True),
    Param('divisorSeries', ParamTypes.seriesList, required=True),
  ]
  
  
  def multiplySeries(requestContext, *seriesLists):
    """
    Takes two or more series and multiplies their points. A constant may not be
    used. To multiply by a constant, use the scale() function.
  
    Example:
  
    .. code-block:: none
  
      &target=multiplySeries(Series.dividends,Series.divisors)
  
    This is an alias for :py:func:`aggregate <aggregate>` with aggregation ``multiply``.
    """
    # special handling for legacy multiplySeries behavior with a single series
    if len(seriesLists) == 1 and len(seriesLists[0]) == 1:
      return seriesLists[0]
  
    return aggregate(requestContext, seriesLists, 'multiply')
  
  
  multiplySeries.group = 'Combine'
  multiplySeries.params = [
    Param('seriesLists', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def weightedAverage(requestContext, seriesListAvg, seriesListWeight, *nodes):
    """
    Takes a series of average values and a series of weights and
    produces a weighted average for all values.
    The corresponding values should share one or more zero-indexed nodes and/or tags.
  
    Example:
  
    .. code-block:: none
  
      &target=weightedAverage(*.transactions.mean,*.transactions.count,0)
  
    Each node may be an integer referencing a node in the series name or a string identifying a tag.
    """
    sortedSeries={}
  
    for seriesAvg, seriesWeight in izip_longest(seriesListAvg , seriesListWeight):
      key = aggKey(seriesAvg, nodes)
  
      if key not in sortedSeries:
        sortedSeries[key]={}
      sortedSeries[key]['avg']=seriesAvg
  
      key = aggKey(seriesWeight, nodes)
  
      if key not in sortedSeries:
        sortedSeries[key]={}
      sortedSeries[key]['weight']=seriesWeight
  
    productList = []
  
    for key in sortedSeries.keys():
      if 'weight' not in sortedSeries[key]:
        continue
      if 'avg' not in sortedSeries[key]:
        continue
  
      seriesWeight = sortedSeries[key]['weight']
      seriesAvg = sortedSeries[key]['avg']
  
      productValues = [ safeMul(val1, val2) for val1,val2 in izip_longest(seriesAvg,seriesWeight) ]
      name='product(%s,%s)' % (seriesWeight.name, seriesAvg.name)
      productSeries = TimeSeries(name,seriesAvg.start,seriesAvg.end,seriesAvg.step,productValues,xFilesFactor=requestContext.get('xFilesFactor'))
      productList.append(productSeries)
  
    if not productList:
      return []
  
    sumProducts=sumSeries(requestContext, productList)[0]
    sumWeights=sumSeries(requestContext, seriesListWeight)[0]
  
    resultValues = [ safeDiv(val1, val2) for val1,val2 in izip_longest(sumProducts,sumWeights) ]
    name = "weightedAverage(%s, %s, %s)" % (','.join(sorted(set(s.pathExpression for s in seriesListAvg))) ,','.join(sorted(set(s.pathExpression for s in seriesListWeight))), ','.join(map(str,nodes)))
    resultSeries = TimeSeries(name,sumProducts.start,sumProducts.end,sumProducts.step,resultValues,xFilesFactor=requestContext.get('xFilesFactor'))
    return [resultSeries]
  
  
  weightedAverage.group = 'Combine'
  weightedAverage.params = [
    Param('seriesListAvg', ParamTypes.seriesList, required=True),
    Param('seriesListWeight', ParamTypes.seriesList, required=True),
    Param('nodes', ParamTypes.nodeOrTag, multiple=True),
  ]
  
  intOrIntervalSuggestions = [5, 7, 10, '1min', '5min', '10min', '30min', '1hour']
  
  
  def movingWindow(requestContext, seriesList, windowSize, func='average', xFilesFactor=None):
    """
    Graphs a moving window function of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList, a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), a function to apply to the points
    in the window to produce the output, and an xFilesFactor value to specify how many points in the
    window must be non-null for the output to be considered valid. Graphs the
    output of the function for the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingWindow(Server.instance01.threads.busy,10)
      &target=movingWindow(Server.instance*.threads.idle,'5min','median',0.5)
  
    .. note::
  
      `xFilesFactor` follows the same semantics as in Whisper storage schemas.  Setting it to 0 (the
      default) means that only a single value in a given interval needs to be non-null, setting it to
      1 means that all values in the interval must be non-null.  A setting of 0.5 means that at least
      half the values in the interval must be non-null.
    """
    if not seriesList:
      return []
  
    if isinstance(windowSize, six.string_types):
      delta = parseTimeOffset(windowSize)
      previewSeconds = abs(delta.seconds + (delta.days * 86400))
    else:
      previewSeconds = max([s.step for s in seriesList]) * int(windowSize)
  
    consolidateFunc = getAggFunc(func)
  
    # ignore original data and pull new, including our preview
    # data from earlier is needed to calculate the early results
    newContext = requestContext.copy()
    newContext['startTime'] = requestContext['startTime'] -  timedelta(seconds=previewSeconds)
    previewList = evaluateTarget(newContext, requestContext['args'][0])
    result = []
  
    tagName = 'moving' + func.capitalize()
  
    for series in previewList:
      if isinstance(windowSize, six.string_types):
        newName = '%s(%s,"%s")' % (tagName, series.name, windowSize)
        windowPoints = previewSeconds // series.step
      else:
        newName = '%s(%s,%s)' % (tagName, series.name, windowSize)
        windowPoints = int(windowSize)
  
      series.tags[tagName] = windowSize
      newSeries = series.copy(name=newName, start=series.start + previewSeconds, values=[])
  
      effectiveXFF = xFilesFactor if xFilesFactor is not None else series.xFilesFactor
  
      for i in range(windowPoints, len(series)):
        nonNull = [v for v in series[i - windowPoints:i] if v is not None]
  
        if nonNull and xff(len(nonNull), windowPoints, effectiveXFF):
          val = consolidateFunc(nonNull)
        else:
          val = None
        newSeries.append(val)
  
      result.append(newSeries)
  
    return result
  
  
  movingWindow.group = 'Calculate'
  movingWindow.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('func', ParamTypes.string, options=aggFuncNames, default='average'),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def exponentialMovingAverage(requestContext, seriesList, windowSize):
    """
    Takes a series of values and a window size and produces an exponential moving
    average utilizing the following formula:
  
    .. code-block:: none
  
      ema(current) = constant * (Current Value) + (1 - constant) * ema(previous)
  
    The Constant is calculated as:
  
    .. code-block:: none
  
      constant = 2 / (windowSize + 1)
  
    The first period EMA uses a simple moving average for its value.
  
    Example:
  
    .. code-block:: none
  
      &target=exponentialMovingAverage(*.transactions.count, 10)
      &target=exponentialMovingAverage(*.transactions.count, '-10s')
  
    """
    # EMA = C * (current_value) + (1 - C) + EMA
    # C = 2 / (windowSize + 1)
  
    # The following was copied from movingAverage, and altered for ema
    if not seriesList:
      return []
    # set previewSeconds and constant based on windowSize string or integer
    if isinstance(windowSize, six.string_types):
      delta = parseTimeOffset(windowSize)
      previewSeconds = abs(delta.seconds + (delta.days * 86400))
      constant = (float(2) / (int(previewSeconds) + 1))
    else:
      previewSeconds = max([s.step for s in seriesList]) * int(windowSize)
      constant = (float(2) / (int(windowSize) + 1))
  
    # ignore original data and pull new, including our preview
    # data from earlier is needed to calculate the early results
    newContext = requestContext.copy()
    newContext['startTime'] = requestContext['startTime'] -  timedelta(seconds=previewSeconds)
    previewList = evaluateTarget(newContext, requestContext['args'][0])
    result = []
  
    for series in previewList:
      if isinstance(windowSize, six.string_types):
        newName = 'exponentialMovingAverage(%s,"%s")' % (series.name, windowSize)
        windowPoints = previewSeconds // series.step
      else:
        newName = "exponentialMovingAverage(%s,%s)" % (series.name, windowSize)
        windowPoints = int(windowSize)
  
      series.tags['exponentialMovingAverage'] = windowSize
      newSeries = series.copy(name=newName, start=series.start + previewSeconds, values=[])
  
      ema = safeAvg(series[:windowPoints]) or 0
      newSeries.append(round(ema, 6))
  
      for i in range(windowPoints, len(series)):
        if series[i] is not None:
          ema = constant * series[i] + (1 - constant) * ema
          newSeries.append(round(ema, 6))
        else:
          newSeries.append(None)
  
      result.append(newSeries)
  
    return result
  
  
  exponentialMovingAverage.group = 'Calculate'
  exponentialMovingAverage.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
  ]
  
  
  def movingMedian(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving median of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    median of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMedian(Server.instance01.threads.busy,10)
      &target=movingMedian(Server.instance*.threads.idle,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'median', xFilesFactor)
  
  
  movingMedian.group = 'Calculate'
  movingMedian.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def scale(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and multiplies the datapoint
    by the constant provided at each point.
  
    Example:
  
    .. code-block:: none
  
      &target=scale(Server.instance01.threads.busy,10)
      &target=scale(Server.instance*.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['scale'] = factor
      series.name = "scale(%s,%g)" % (series.name,float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safeMul(value,factor)
    return seriesList
  
  
  scale.group = 'Transform'
  scale.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def scaleToSeconds(requestContext, seriesList, seconds):
    """
    Takes one metric or a wildcard seriesList and returns "value per seconds" where
    seconds is a last argument to this functions.
  
    Useful in conjunction with derivative or integral function if you want
    to normalize its result to a known resolution for arbitrary retentions
    """
  
    for series in seriesList:
      series.tags['scaleToSeconds'] = seconds
      series.name = "scaleToSeconds(%s,%d)" % (series.name,seconds)
      series.pathExpression = series.name
      factor = seconds / series.step
  
      for i,value in enumerate(series):
        if value is not None:
          # Division long by float cause OverflowError
          try:
            series[i] = round(value * factor, 6)
          except OverflowError:
            series[i] = (value // series.step) * seconds
  
    return seriesList
  
  
  scaleToSeconds.group = 'Transform'
  scaleToSeconds.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('seconds', ParamTypes.integer, required=True),
  ]
  
  
  def pow(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and raises the datapoint
    by the power of the constant provided at each point.
  
    Example:
  
    .. code-block:: none
  
      &target=pow(Server.instance01.threads.busy,10)
      &target=pow(Server.instance*.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['pow'] = factor
      series.name = "pow(%s,%g)" % (series.name, float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safePow(value, factor)
    return seriesList
  
  
  pow.group = 'Transform'
  pow.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def powSeries(requestContext, *seriesLists):
    """
    Takes two or more series and pows their points. A constant line may be
    used.
  
    Example:
  
    .. code-block:: none
  
      &target=powSeries(Server.instance01.app.requests, Server.instance01.app.replies)
  
  
    """
  
    try:
      (seriesList, start, end, step) = normalize(seriesLists)
    except NormalizeEmptyResultError:
      return []
    name = "powSeries(%s)" % ','.join([s.name for s in seriesList])
    values = []
    for row in izip_longest(*seriesList):
      first = True
      tmpVal = None
      for element in row:
        # If it is a first iteration - tmpVal needs to be element
        if first:
          tmpVal = element
          first = False
        else:
          tmpVal = safePow(tmpVal, element)
      values.append(tmpVal)
    series = TimeSeries(name,start,end,step,values,xFilesFactor=requestContext.get('xFilesFactor'))
    return [series]
  
  
  powSeries.group = 'Transform'
  powSeries.params = [
    Param('seriesList', ParamTypes.seriesList, required=True, multiple=True),
  ]
  
  
  def squareRoot(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList, and computes the square root of each datapoint.
  
    Example:
  
    .. code-block:: none
  
      &target=squareRoot(Server.instance01.threads.busy)
  
    """
    for series in seriesList:
      series.tags['squareRoot'] = 1
      series.name = "squareRoot(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safePow(value, 0.5)
    return seriesList
  
  
  squareRoot.group = 'Transform'
  squareRoot.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def invert(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList, and inverts each datapoint (i.e. 1/x).
  
    Example:
  
    .. code-block:: none
  
      &target=invert(Server.instance01.threads.busy)
  
    """
    for series in seriesList:
      series.tags['invert'] = 1
      series.name = "invert(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
          series[i] = safePow(value, -1)
    return seriesList
  
  
  invert.group = 'Transform'
  invert.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def absolute(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList and applies the mathematical abs function to each
    datapoint transforming it to its absolute value.
  
    Example:
  
    .. code-block:: none
  
      &target=absolute(Server.instance01.threads.busy)
      &target=absolute(Server.instance*.threads.busy)
    """
    for series in seriesList:
      series.tags['absolute'] = 1
      series.name = "absolute(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        series[i] = safeAbs(value)
    return seriesList
  
  
  absolute.group = 'Transform'
  absolute.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def offset(requestContext, seriesList, factor):
    """
    Takes one metric or a wildcard seriesList followed by a constant, and adds the constant to
    each datapoint.
  
    Example:
  
    .. code-block:: none
  
      &target=offset(Server.instance01.threads.busy,10)
  
    """
    for series in seriesList:
      series.tags['offset'] = factor
      series.name = "offset(%s,%g)" % (series.name,float(factor))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = value + factor
    return seriesList
  
  
  offset.group = 'Transform'
  offset.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('factor', ParamTypes.float, required=True),
  ]
  
  
  def offsetToZero(requestContext, seriesList):
    """
    Offsets a metric or wildcard seriesList by subtracting the minimum
    value in the series from each datapoint.
  
    Useful to compare different series where the values in each series
    may be higher or lower on average but you're only interested in the
    relative difference.
  
    An example use case is for comparing different round trip time
    results. When measuring RTT (like pinging a server), different
    devices may come back with consistently different results due to
    network latency which will be different depending on how many
    network hops between the probe and the device. To compare different
    devices in the same graph, the network latency to each has to be
    factored out of the results. This is a shortcut that takes the
    fastest response (lowest number in the series) and sets that to zero
    and then offsets all of the other datapoints in that series by that
    amount. This makes the assumption that the lowest response is the
    fastest the device can respond, of course the more datapoints that
    are in the series the more accurate this assumption is.
  
    Example:
  
    .. code-block:: none
  
      &target=offsetToZero(Server.instance01.responseTime)
      &target=offsetToZero(Server.instance*.responseTime)
  
    """
    for series in seriesList:
      minimum = safeMin(series)
      series.tags['offsetToZero'] = minimum
      series.name = "offsetToZero(%s)" % (series.name)
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = value - minimum
    return seriesList
  
  
  offsetToZero.group = 'Transform'
  offsetToZero.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def roundFunction(requestContext, seriesList, precision=None):
    """
    Takes one metric or a wildcard seriesList optionally followed by a precision, and rounds each
    datapoint to the specified precision.
  
    Example:
  
    .. code-block:: none
  
      &target=round(Server.instance01.threads.busy)
      &target=round(Server.instance01.threads.busy,2)
  
    """
    for series in seriesList:
      series.tags['round'] = precision or 0
      if precision is None:
        series.name = "round(%s)" % (series.name)
      else:
        series.name = "round(%s,%g)" % (series.name,int(precision))
      series.pathExpression = series.name
      for i,value in enumerate(series):
        if value is not None:
          series[i] = round(value, precision or 0)
    return seriesList
  
  
  roundFunction.group = 'Transform'
  roundFunction.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('precision', ParamTypes.integer, default=0),
  ]
  
  
  def movingAverage(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving average of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    average of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingAverage(Server.instance01.threads.busy,10)
      &target=movingAverage(Server.instance*.threads.idle,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'average', xFilesFactor)
  
  
  movingAverage.group = 'Calculate'
  movingAverage.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingSum(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving sum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    sum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingSum(Server.instance01.requests,10)
      &target=movingSum(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'sum', xFilesFactor)
  
  
  movingSum.group = 'Calculate'
  movingSum.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingMin(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving minimum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    minimum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMin(Server.instance01.requests,10)
      &target=movingMin(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'min', xFilesFactor)
  
  
  movingMin.group = 'Calculate'
  movingMin.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def movingMax(requestContext, seriesList, windowSize, xFilesFactor=None):
    """
    Graphs the moving maximum of a metric (or metrics) over a fixed number of
    past points, or a time interval.
  
    Takes one metric or a wildcard seriesList followed by a number N of datapoints
    or a quoted string with a length of time like '1hour' or '5min' (See ``from /
    until`` in the :doc:`Render API <render_api>` for examples of time formats), and an xFilesFactor value to specify
    how many points in the window must be non-null for the output to be considered valid. Graphs the
    maximum of the preceeding datapoints for each point on the graph.
  
    Example:
  
    .. code-block:: none
  
      &target=movingMax(Server.instance01.requests,10)
      &target=movingMax(Server.instance*.errors,'5min')
  
    """
    return movingWindow(requestContext, seriesList, windowSize, 'max', xFilesFactor)
  
  
  movingMax.group = 'Calculate'
  movingMax.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('windowSize', ParamTypes.intOrInterval, required=True, suggestions=intOrIntervalSuggestions),
    Param('xFilesFactor', ParamTypes.float),
  ]
  
  
  def cumulative(requestContext, seriesList):
    """
    Takes one metric or a wildcard seriesList.
  
    When a graph is drawn where width of the graph size in pixels is smaller than
    the number of datapoints to be graphed, Graphite consolidates the values to
    to prevent line overlap. The cumulative() function changes the consolidation
    function from the default of 'average' to 'sum'. This is especially useful in
    sales graphs, where fractional values make no sense and a 'sum' of consolidated
    values is appropriate.
  
    Alias for :func:`consolidateBy(series, 'sum') <graphite.render.functions.consolidateBy>`
  
    .. code-block:: none
  
      &target=cumulative(Sales.widgets.largeBlue)
  
    """
    return consolidateBy(requestContext, seriesList, 'sum')
  
  
  cumulative.group = 'Special'
  cumulative.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
  ]
  
  
  def consolidateBy(requestContext, seriesList, consolidationFunc):
    """
    Takes one metric or a wildcard seriesList and a consolidation function name.
  
    Valid function names are 'sum', 'average', 'min', 'max', 'first' & 'last'.
  
    When a graph is drawn where width of the graph size in pixels is smaller than
    the number of datapoints to be graphed, Graphite consolidates the values to
    to prevent line overlap. The consolidateBy() function changes the consolidation
    function from the default of 'average' to one of 'sum', 'max', 'min', 'first', or 'last'.
    This is especially useful in sales graphs, where fractional values make no sense and a 'sum'
    of consolidated values is appropriate.
  
    .. code-block:: none
  
      &target=consolidateBy(Sales.widgets.largeBlue, 'sum')
      &target=consolidateBy(Servers.web01.sda1.free_space, 'max')
  
    """
    for series in seriesList:
      # datalib will throw an exception, so it's not necessary to validate here
      series.consolidationFunc = consolidationFunc
      series.tags['consolidateBy'] = consolidationFunc
      series.name = 'consolidateBy(%s,"%s")' % (series.name, series.consolidationFunc)
      series.pathExpression = series.name
    return seriesList
  
  
  consolidateBy.group = 'Special'
  consolidateBy.params = [
    Param('seriesList', ParamTypes.seriesList, required=True),
    Param('consolidationFunc', ParamTypes.string, required=True, options=['sum', 'average', 'min', 'max', 'first', 'last']),
  ]
  
  
  def setXFilesFactor(requestContext, seriesList, xFilesFactor):
    """
    Short form: xFilesFactor()
  
    Takes one metric or a wildcard seriesList and an xFilesFactor value between 0 and 1
  
    When a series needs to be consolidated, this sets the fraction of values in an interval that must
    not be null for the consolidation to be considered valid.  If there are not enough values then
    None will be returned for that interval.
  
    .. code-block:: none
  
      &target=xFilesFactor(Sales.widgets.largeBlue, 0.5)
      &target=Servers.web01.sda1.free_space|consolidateBy('max')|xFilesFactor(0.5)
  
    The `xFilesFactor` set via this function is used as the default for all functions that accept an
    `xFilesFactor` parameter, all functions that aggregate data across multiple series and/or
    intervals, and `maxDataPoints <render_api.html#maxdatapoints>`_ consolidation.
  
    A default for the entire render request can also be set using the
    `xFilesFactor <render_api.html#xfilesfactor>`_ query parameter.
  
    .. note::
  
      `xFilesFactor` follows the same semantics as in Whisper storage schemas.  Setting it to 0 (the
      default) means that only a single value in a given interval needs to be non-null, setting it to
      1 means that all values in the interval must be non-null.  A setting of 0.5 means that at least
      half
  
  [... the rest is omitted ...]
files.old_path: webapp/graphite/render/functions.py
files.new_contents: import copy
  import inspect
  import math
  import pytz
  import six
  
  from datetime import datetime
  from fnmatch import fnmatch
  from mock import patch, call, MagicMock
  from os.path import dirname, join
  from six.moves import range
  
  from .base import TestCase
  from django.conf import settings
  
  try:
    from django.urls import reverse
  except ImportError:  # Django < 1.10
    from django.core.urlresolvers import reverse
  
  from graphite.errors import NormalizeEmptyResultError
  from graphite.functions import _SeriesFunctions, loadFunctions
  from graphite.render.datalib import TimeSeries
  from graphite.render import functions
  from graphite.render.evaluator import evaluateTarget
  from graphite.render.grammar import grammar
  from graphite.tags.utils import TaggedSeries
  from graphite.util import json
  
  
  def return_greater(series, value):
      return [i for i in series if i is not None and i > value]
  
  
  def return_less(series, value):
      return [i for i in series if i is not None and i < value]
  
  
  class FunctionsTest(TestCase):
  
      def setUp(self):
        super(FunctionsTest, self).setUp()
        # Display more diff.
        self.maxDiff = 1024
  
      #
      # Test safeSum()
      #
  
      def test_safeSum_None(self):
          with self.assertRaises(TypeError):
              functions.safeSum(None)
  
      def test_safeSum_empty_list(self):
          self.assertEqual(functions.safeSum([]), None)
  
      def test_safeSum_all_numbers(self):
          self.assertEqual(functions.safeSum([1,2,3,4]), 10)
  
      def test_safeSum_all_None(self):
          self.assertEqual(functions.safeSum([None,None,None,None]), None)
  
      def test_safeSum_mixed(self):
          self.assertEqual(functions.safeSum([10,None,5,None]), 15)
  
      #
      # Test safeDiff()
      #
  
      def test_safeDiff_None(self):
          with self.assertRaises(TypeError):
              functions.safeDiff(None)
  
      def test_safeDiff_empty_list(self):
          self.assertEqual(functions.safeDiff([]), None)
  
      def test_safeDiff_all_numbers(self):
          self.assertEqual(functions.safeDiff([1,2,3,4]), -8)
  
      def test_safeDiff_all_None(self):
          self.assertEqual(functions.safeDiff([None,None,None,None]), None)
  
      def test_safeDiff_mixed(self):
          self.assertEqual(functions.safeDiff([10,None,5,None]), 5)
  
      #
      # Test safeLen()
      #
  
      def test_safeLen_None(self):
          with self.assertRaises(TypeError):
              functions.safeLen(None)
  
      def test_safeLen_empty_list(self):
          self.assertEqual(functions.safeLen([]), 0)
  
      def test_safeLen_all_numbers(self):
          self.assertEqual(functions.safeLen([1,2,3,4]), 4)
  
      def test_safeLen_all_None(self):
          self.assertEqual(functions.safeLen([None,None,None,None]), 0)
  
      def test_safeLen_mixed(self):
          self.assertEqual(functions.safeLen([10,None,5,None]), 2)
  
      #
      # Test safeDiv()
      #
  
      def test_safeDiv_None_None(self):
          self.assertEqual(functions.safeDiv(None, None), None)
  
      def test_safeDiv_5_None(self):
          self.assertEqual(functions.safeDiv(5, None), None)
  
      def test_safeDiv_5_0(self):
          self.assertEqual(functions.safeDiv(5, 0), None)
  
      def test_safeDiv_0_10(self):
          self.assertEqual(functions.safeDiv(0,10), 0)
  
      def test_safeDiv_10_5(self):
          self.assertEqual(functions.safeDiv(10,5), 2)
  
      #
      # Test safePow()
      #
  
      def test_safePow_None_None(self):
          self.assertEqual(functions.safePow(None, None), None)
  
      def test_safePow_5_None(self):
          self.assertEqual(functions.safePow(5, None), None)
  
      def test_safePow_5_0(self):
          self.assertEqual(functions.safePow(5, 0), 1.0)
  
      def test_safePow_0_10(self):
          self.assertEqual(functions.safePow(0,10), 0)
  
      def test_safePow_10_5(self):
          self.assertEqual(functions.safePow(10,5), 100000.0)
  
      #
      # Test safeMul()
      #
  
      def test_safeMul_None_None(self):
          self.assertEqual(functions.safeMul(None, None), None)
  
      def test_safeMul_5_None(self):
          self.assertEqual(functions.safeMul(5, None), None)
  
      def test_safeMul_5_0(self):
          self.assertEqual(functions.safeMul(5, 0), 0.0)
  
      def test_safeMul_0_10(self):
          self.assertEqual(functions.safeMul(0,10), 0)
  
      def test_safeMul_10_5(self):
          self.assertEqual(functions.safeMul(10,5), 50.0)
  
      #
      # Test safeSubtract()
      #
  
      def test_safeSubtract_None_None(self):
          self.assertEqual(functions.safeSubtract(None, None), None)
  
      def test_safeSubtract_5_None(self):
          self.assertEqual(functions.safeSubtract(5, None), None)
  
      def test_safeSubtract_5_0(self):
          self.assertEqual(functions.safeSubtract(5, 0), 5.0)
  
      def test_safeSubtract_0_10(self):
          self.assertEqual(functions.safeSubtract(0,10), -10)
  
      def test_safeSubtract_10_5(self):
          self.assertEqual(functions.safeSubtract(10,5), 5)
  
      #
      # Test safeAvg()
      #
  
      def test_safeAvg_None(self):
          with self.assertRaises(TypeError):
              functions.safeAvg(None)
  
      def test_safeAvg_empty_list(self):
          self.assertEqual(functions.safeAvg([]), None)
  
      def test_safeAvg_all_numbers(self):
          self.assertEqual(functions.safeAvg([1,2,3,4]), 2.5)
  
      def test_safeAvg_all_None(self):
          self.assertEqual(functions.safeAvg([None,None,None,None]), None)
  
      def test_safeAvg_mixed(self):
          self.assertEqual(functions.safeAvg([10,None,5,None]), 7.5)
  
      #
      # Test safeMedian()
      #
  
      def test_safeMedian_None(self):
          with self.assertRaises(TypeError):
              functions.safeMedian(None)
  
      def test_safeMedian_empty_list(self):
          self.assertEqual(functions.safeMedian([]), None)
  
      def test_safeMedian_all_numbers_odd_len(self):
          self.assertEqual(functions.safeMedian([1,2,3,4,5]), 3)
  
      def test_safeMedian_all_numbers_even_len(self):
          self.assertAlmostEqual(functions.safeMedian([1,2,3,4]), 2.5)
  
      def test_safeMedian_all_None(self):
          self.assertEqual(functions.safeMedian([None,None,None,None]), None)
  
      def test_safeMedian_mixed(self):
          self.assertAlmostEqual(functions.safeMedian([10,None,5,None]), 7.5)
  
      #
      # Test safeStdDev()
      #
  
      def test_safeStdDev_None(self):
          with self.assertRaises(TypeError):
              functions.safeStdDev(None)
  
      def test_safeStdDev_empty_list(self):
          self.assertEqual(functions.safeStdDev([]), None)
  
      def test_safeStdDev_all_numbers(self):
          self.assertEqual(functions.safeStdDev([1,2,3,4]), 1.118033988749895)
  
      def test_safeStdDev_all_None(self):
          self.assertEqual(functions.safeStdDev([None,None,None,None]), None)
  
      def test_safeStdDev_mixed(self):
          self.assertEqual(functions.safeStdDev([10,None,5,None]), 2.5)
  
      #
      # Test safeLast()
      #
  
      def test_safeLast_None(self):
          with self.assertRaises(TypeError):
              functions.safeLast(None)
  
      def test_safeLast_empty_list(self):
          self.assertEqual(functions.safeLast([]), None)
  
      def test_safeLast_all_numbers(self):
          self.assertEqual(functions.safeLast([1,2,3,4]), 4)
  
      def test_safeLast_all_None(self):
          self.assertEqual(functions.safeLast([None,None,None,None]), None)
  
      def test_safeLast_mixed(self):
          self.assertEqual(functions.safeLast([10,None,5,None]), 5)
  
      #
      # Test safeMin()
      #
  
      def test_safeMin_None(self):
          with self.assertRaises(TypeError):
              functions.safeMin(None)
  
      def test_safeMin_empty_list(self):
          self.assertEqual(functions.safeMin([]), None)
  
      def test_safeMin_all_numbers(self):
          self.assertEqual(functions.safeMin([1,2,3,4]), 1)
  
      def test_safeMin_all_None(self):
          self.assertEqual(functions.safeMin([None,None,None,None]), None)
  
      def test_safeMin_mixed(self):
          self.assertEqual(functions.safeMin([10,None,5,None]), 5)
  
      #
      # Test safeMax()
      #
  
      def test_safeMax_None(self):
          with self.assertRaises(TypeError):
              functions.safeMax(None)
  
      def test_safeMax_empty_list(self):
          self.assertEqual(functions.safeMax([]), None)
  
      def test_safeMax_all_numbers(self):
          self.assertEqual(functions.safeMax([1,2,3,4]), 4)
  
      def test_safeMax_all_None(self):
          self.assertEqual(functions.safeMax([None,None,None,None]), None)
  
      def test_safeMax_mixed(self):
          self.assertEqual(functions.safeMax([10,None,5,None]), 10)
  
      #
      # Test safeAbs()
      #
  
      def test_safeAbs_None(self):
          self.assertEqual(functions.safeAbs(None), None)
  
      def test_safeAbs_empty_list(self):
          with self.assertRaises(TypeError):
            functions.safeAbs([])
  
      def test_safeAbs_pos_number(self):
          self.assertEqual(functions.safeAbs(1), 1)
  
      def test_safeAbs_neg_numbers(self):
          self.assertEqual(functions.safeAbs(-1), 1)
  
      def test_safeAbs_zero(self):
          self.assertEqual(functions.safeAbs(0), 0)
  
      #
      # Test safeMap()
      #
  
      def test_safeMap_None(self):
          with self.assertRaises(TypeError):
              functions.safeMap(abs, None)
  
      def test_safeMap_empty_list(self):
          self.assertEqual(functions.safeMap(abs, []), None)
  
      def test_safeMap_all_numbers(self):
          self.assertEqual(functions.safeMap(abs, [1,2,3,4]), [1,2,3,4])
  
      def test_safeMap_all_None(self):
          self.assertEqual(functions.safeMap(abs, [None,None,None,None]), None)
  
      def test_safeMap_mixed(self):
          self.assertEqual(functions.safeMap(abs, [10,None,5,None]), [10,5])
  
      #
      # Test gcd()
      #
  
      def test_gcd_None_None(self):
          with self.assertRaises(TypeError):
              functions.gcd(None, None)
  
      def test_gcd_5_None(self):
          with self.assertRaises(TypeError):
              functions.gcd(5, None)
  
      def test_gcd_5_0(self):
          self.assertEqual(functions.gcd(5, 0), 5)
  
      def test_gcd_0_10(self):
          self.assertEqual(functions.gcd(0,10), 10)
  
      def test_gcd_10_5(self):
          self.assertEqual(functions.gcd(10,5), 5)
  
      #
      # Test lcm()
      #
  
      def test_lcm_None_None(self):
          self.assertEqual(functions.lcm(None, None), None)
  
      def test_lcm_5_None(self):
          with self.assertRaises(TypeError):
              functions.lcm(5, None)
  
      def test_lcm_5_0(self):
          self.assertEqual(functions.lcm(5, 0), 0)
  
      def test_lcm_0_10(self):
          self.assertEqual(functions.lcm(0,10), 0)
  
      def test_lcm_10_5(self):
          self.assertEqual(functions.lcm(10,5), 10)
  
      #
      # Test xffValues()
      #
  
      def test_xffValues_None(self):
          self.assertEqual(functions.xffValues(None, 0.5), False)
  
      def test_xffValues_empty_list(self):
          self.assertEqual(functions.xffValues([], 0.5), False)
  
      def test_xffValues_all_numbers(self):
          self.assertEqual(functions.xffValues([1,2,3,4], 0.5), True)
  
      def test_xffValues_all_None(self):
          self.assertEqual(functions.xffValues([None,None,None,None], 0.5), False)
  
      def test_xffValues_mixed(self):
          self.assertEqual(functions.xffValues([10,None,5,None], 0.5), True)
  
      #
      # Test xff()
      #
  
      def test_xff_None(self):
          self.assertEqual(functions.xff(None, None, 0.5), False)
  
      def test_xff(self):
          self.assertEqual(functions.xff(1, 2, 0.5), True)
  
      #
      # Test normalize()
      #
  
      def test_normalize_empty(self):
          with self.assertRaises(NormalizeEmptyResultError):
              functions.normalize([])
  
      def test_normalize_None_values(self):
          seriesList = []
          seriesList.append(TimeSeries("collectd.test-db{0}.load.value", 0, 5, 1, [None, None, None, None, None]))
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 5, 1))
  
      def test_normalize_generate_series_list_input(self):
          seriesList = self._generate_series_list()
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 101, 1))
  
      def test_normalize_different_steps(self):
          seriesList = [
            TimeSeries("test.1", 0, 120 * 31, 120, values=list(range(1, 32))),
            TimeSeries("test.2", 0, 30 * 120, 30, values=list(range(1, 121))),
          ]
  
          self.assertEqual(seriesList[0].start, 0)
          self.assertEqual(seriesList[0].end, 120 * 31)
          self.assertEqual(seriesList[0].step, 120)
          self.assertEqual(seriesList[0].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[0])), 31)
  
          self.assertEqual(seriesList[1].start, 0)
          self.assertEqual(seriesList[1].end, 30 * 120)
          self.assertEqual(seriesList[1].step, 30)
          self.assertEqual(seriesList[1].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[1])), 120)
  
          # normalize seriesList
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 3720, 120))
  
          self.assertEqual(seriesList[0].start, 0)
          self.assertEqual(seriesList[0].end, 120 * 31)
          self.assertEqual(seriesList[0].step, 120)
          self.assertEqual(seriesList[0].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[0])), 31)
  
          self.assertEqual(seriesList[1].start, 0)
          self.assertEqual(seriesList[1].end, 30 * 120)
          self.assertEqual(seriesList[1].step, 30)
          self.assertEqual(seriesList[1].valuesPerPoint, 4)
          self.assertEqual(len(list(seriesList[1])), 30)
  
      #
      # Test matchSeries()
      #
      def test_matchSeries_assert(self):
          seriesList = self._generate_series_list()
          with self.assertRaisesRegexp(AssertionError, 'The number of series in each argument must be the same'):
              functions.matchSeries(seriesList[0], [])
  
      def test_matchSeries_empty(self):
          results=functions.matchSeries([],[])
          for i, (series1, series2) in enumerate(results):
              self.assertEqual(series1, [])
              self.assertEqual(series2, [])
  
      def test_matchSeries(self):
          seriesList1 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db3.load.value',
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db4.load.value'
              ],
              end=1,
              data=[
                  [3,30,31],
                  [1,10,11],
                  [2,20,21],
                  [4,40,41]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db4.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value'
              ],
              end=1,
              data=[
                  [4,8,12],
                  [3,7,11],
                  [1,5,9],
                  [2,6,10]
              ]
          )
  
          expectedResult = [
          [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[1,10,11]),
              TimeSeries('collectd.test-db2.load.value',0,1,1,[2,20,21]),
              TimeSeries('collectd.test-db3.load.value',0,1,1,[3,30,31]),
              TimeSeries('collectd.test-db4.load.value',0,1,1,[4,40,41]),
          ],
          [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[1,5,9]),
              TimeSeries('collectd.test-db2.load.value',0,1,1,[2,6,10]),
              TimeSeries('collectd.test-db3.load.value',0,1,1,[3,7,11]),
              TimeSeries('collectd.test-db4.load.value',0,1,1,[4,8,12]),
          ]]
          results = functions.matchSeries(copy.deepcopy(seriesList1), copy.deepcopy(seriesList2))
          for i, (series1, series2) in enumerate(results):
              self.assertEqual(series1, expectedResult[0][i])
              self.assertEqual(series2, expectedResult[1][i])
  
      #
      # Test formatPathExpressions()
      #
  
      def test_formatPathExpressions_empty_list(self):
          self.assertEqual(functions.formatPathExpressions([]), '')
  
      def test_formatPathExpressions(self):
          seriesList = self._generate_series_list()
          self.assertEqual(functions.formatPathExpressions(seriesList), "collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value")
  
      #
      # test aggregate()
      #
  
      def test_aggregate_tags(self):
          seriesList = self._gen_series_list_with_data(
              key=['disk.bytes_used;server=server1', 'disk.bytes_free;server=server1'],
              start=0,
              end=3,
              data=[[10, 20, 30], [90, 80, 70]]
          )
  
          result = functions.aggregate({}, seriesList, 'sum')
          self.assertEqual(result, [
              TimeSeries('sumSeries(disk.bytes_used;server=server1,disk.bytes_free;server=server1)', 0, 3, 1, [100, 100, 100]),
          ])
          self.assertEqual(result[0].tags, {
            'name': 'sumSeries(disk.bytes_used;server=server1,disk.bytes_free;server=server1)',
            'server': 'server1',
            'aggregatedBy': 'sum',
          })
  
          seriesList = self._gen_series_list_with_data(
              key=['disk.bytes_used;server=server1', 'disk.bytes_used;server=server2'],
              start=0,
              end=3,
              data=[[10, 20, 30], [90, 80, 70]]
          )
  
          result = functions.aggregate({}, seriesList, 'sum')
          self.assertEqual(result, [
              TimeSeries('sumSeries(disk.bytes_used;server=server1,disk.bytes_used;server=server2)', 0, 3, 1, [100, 100, 100]),
          ])
          self.assertEqual(result[0].tags, {
            'name': 'disk.bytes_used',
            'aggregatedBy': 'sum',
          })
  
      #
      # Test sumSeries()
      #
  
      def test_sumSeries_empty(self):
          self.assertEqual(functions.sumSeries({}, []), [])
  
      def test_sumSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,202,2))
          expected_name = "sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.sumSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_sumSeriesWithWildcards_empty_series_int_position(self):
          self.assertEqual(functions.sumSeriesWithWildcards({}, [], 0), [])
  
      def test_sumSeriesWithWildcards(self):
          seriesList = self._generate_series_list()
          data = list(range(0,202,2))
          expected_name = "load.value"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.sumSeriesWithWildcards({}, [seriesList[0], seriesList[1]], 0,1)
          self.assertEqual(result, expectedList)
  
      def test_averageSeriesWithWildcards_empty_series_int_position(self):
          self.assertEqual(functions.averageSeriesWithWildcards({}, [], 0), [])
  
      def test_averageSeriesWithWildcards(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101,1))
          expected_name = "load.value"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.averageSeriesWithWildcards({}, [seriesList[0], seriesList[1]], 0,1)
          self.assertEqual(result, expectedList)
  
      def test_multiplySeriesWithWildcards(self):
          seriesList1 = self._gen_series_list_with_data(
              key=[
                  'web.host-1.avg-response.value',
                  'web.host-2.avg-response.value',
                  'web.host-3.avg-response.value',
                  'web.host-4.avg-response.value'
              ],
              end=1,
              data=[
                  [1,10,11],
                  [2,20,21],
                  [3,30,31],
                  [4,40,41]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'web.host-4.avg-response.value',
                  'web.host-3.avg-response.value',
                  'web.host-1.avg-response.value',
                  'web.host-2.avg-response.value'
              ],
              end=1,
              data=[
                  [4,8,12],
                  [3,7,11],
                  [1,5,9],
                  [2,6,10]
              ]
          )
  
          expectedResult = [
              TimeSeries('web.host-1',0,1,1,[1,50,99]),
              TimeSeries('web.host-2',0,1,1,[4,120,210]),
              TimeSeries('web.host-3',0,1,1,[9,210,341]),
              TimeSeries('web.host-4',0,1,1,[16,320,492]),
          ]
  
          results = functions.multiplySeriesWithWildcards({}, copy.deepcopy(seriesList1+seriesList2), 2,3)
          self.assertEqual(results,expectedResult)
  
      def test_diffSeries(self):
          seriesList = self._generate_series_list()
          data = [0] * 101
          expected_name = "diffSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.diffSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_aggregate_median(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "medianSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.aggregate({}, [seriesList[0], seriesList[1]], 'median')
          self.assertEqual(result, expectedList)
  
      def test_aggregate_stripSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "medianSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.aggregate({}, [seriesList[0], seriesList[1]], 'medianSeries')
          self.assertEqual(result, expectedList)
  
      def test_aggregate_invalidFunc(self):
          with self.assertRaisesRegexp(Exception, "Unsupported aggregation function: blahSeries"):
              functions.aggregate({}, [], 'blahSeries')
  
      def test_aggregate_emptySeries(self):
          result = functions.aggregate({}, [], 'sum')
          self.assertEqual(result, [])
  
      def test_averageSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "averageSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.averageSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_stddevSeries(self):
          seriesList = self._generate_series_list()
          data = [0.0] * 101
          expected_name = "stddevSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.stddevSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_minSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "minSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.minSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_maxSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "maxSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.maxSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_rangeOfSeries(self):
          seriesList = self._generate_series_list()
          data = [0.0] * 101
          expected_name = "rangeOfSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.rangeOfSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_percentileOfSeries_0th_percentile(self):
          with self.assertRaisesRegexp(ValueError, 'The requested percent is required to be greater than 0'):
              functions.percentileOfSeries({}, [], 0)
  
      def test_percentileOfSeries_empty_series(self):
          series = []
          expected = []
          result = functions.percentileOfSeries({}, series, 90)
          self.assertEqual(result, expected)
  
      def test_percentileOfSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "percentileOfSeries(collectd.test-db1.load.value,90)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.percentileOfSeries({}, [seriesList[0], seriesList[1]], 90)
          self.assertEqual(result, expectedList)
  
      def testGetPercentile_empty_points(self):
          self.assertEqual(functions._getPercentile([], 30), None)
  
      def testGetPercentile_percentile_0(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 15),
              (list(range(100)), 0),
              (list(range(200)), 0),
              (list(range(300)), 0),
              (list(range(1, 101)), 1),
              (list(range(1, 201)), 1),
              (list(range(1, 301)), 1),
              (list(range(0, 102)), 0),
              (list(range(1, 203)), 1),
              (list(range(1, 303)), 1),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 0, True)
              self.assertEqual(expected, result, 'For series index <%s> the 0th percentile ordinal is not %d, but %d ' % (index, expected, result))
  
      def testGetPercentile_interpolated(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 19.0),
              (list(range(100)), 29.3),
              (list(range(200)), 59.3),
              (list(range(300)), 89.3),
              (list(range(1, 101)), 30.3),
              (list(range(1, 201)), 60.3),
              (list(range(1, 301)), 90.3),
              (list(range(0, 102)), 29.9),
              (list(range(1, 203)), 60.9),
              (list(range(1, 303)), 90.9),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 30, True)
              self.assertAlmostEqual(expected, result, 4, 'For series index <%s> the 30th percentile ordinal is not %g, but %g' % (index, expected, result))
  
      def testGetPercentile(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 20),
              (list(range(100)), 30),
              (list(range(200)), 60),
              (list(range(300)), 90),
              (list(range(1, 101)), 31),
              (list(range(1, 201)), 61),
              (list(range(1, 301)), 91),
              (list(range(0, 102)), 30),
              (list(range(1, 203)), 61),
              (list(range(1, 303)), 91),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 30)
              self.assertEqual(expected, result, 'For series index <%s> the 30th percentile ordinal is not %d, but %d ' % (index, expected, result))
  
      def test_keepLastValue(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value',
                  'collectd.test-db6.load.value',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,None,3,None,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,None],
              ]
          )
  
          expectedResult = [
              TimeSeries('keepLastValue(collectd.test-db1.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('keepLastValue(collectd.test-db2.load.value)',0,1,1,[None,2,2,4,4,6,6,8,8,10,10,12,12,14,14,16,16,18,18,20]),
              TimeSeries('keepLastValue(collectd.test-db3.load.value)',0,1,1,[1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None]),
              TimeSeries('keepLastValue(collectd.test-db4.load.value)',0,1,1,[1,2,3,4,4,6,6,6,9,10,11,11,13,None,None,None,None,18,19,20]),
              TimeSeries('keepLastValue(collectd.test-db5.load.value)',0,1,1,[1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,18,18]),
              TimeSeries('keepLastValue(collectd.test-db6.load.value)',0,1,1,[1,1,3,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,19]),
          ]
          results = functions.keepLastValue({}, seriesList, 2)
          self.assertEqual(results, expectedResult)
  
      def test_interpolate(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('interpolate(collectd.test-db1.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db2.load.value)',0,1,1,[None,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db3.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None]),
              TimeSeries('interpolate(collectd.test-db4.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db5.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]),
          ]
          results = functions.interpolate({}, seriesList)
          self.assertEqual(results, expectedResult)
  
      def test_changed(self):
          config = [
              [[1,2,3,4,4,5,5,5,6,7], [0,1,1,1,0,1,0,0,1,1]],
              [[None,None,None,None,0,0,0,None,None,1], [0,0,0,0,0,0,0,0,0,1]]
          ]
          for i, c in enumerate(config):
              name = "collectd.test-db{0}.load.value".format(i + 1)
              series = [TimeSeries(name,0,1,1,c[0])]
              expected = [TimeSeries("changed(%s)" % name,0,1,1,c[1])]
              result = functions.changed({}, series)
              self.assertEqual(result, expected)
  
      def test_delay(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(18)) + [None, None]),
          ]
          delay = 2
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,2)',0,1,1,[None, None] + list(range(18))),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_too_many_steps(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(20))),
          ]
          delay = 25
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,25)',0,1,1,[None]*20),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_negative(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[None,None] + list(range(2,20))),
          ]
          delay = -2
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,-2)',0,1,1,list(range(2,20)) + [None,None]),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_negative_too_many_steps(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(20))),
          ]
          delay = -25
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,-25)',0,1,1,[None]*20),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_asPercent_error(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db1.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
              ]
          )
  
          with self.assertRaisesRegexp(ValueError, "asPercent second argument must be missing, a single digit, reference exactly 1 series or reference the same number of series as the first argument"):
              functions.asPercent({}, seriesList, seriesList2)
  
      def test_asPercent_no_seriesList2(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, 50.0, 33.33, 100.0, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, 25.0, 50.0, 33.33]),
              TimeSeries('asPercent(collectd.test-db2.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[None, 20.0, None, 33.33, None, 20.0, None, 25.0, None, 20.0, None, 25.0, None, 25.0, None, 25.0, None, 25.0, None, 33.33]),
              TimeSeries('asPercent(collectd.test-db3.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, None, None, None, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, 50.0, 33.33, None, 20.0, None, None, 25.0, 20.0, 25.0, None, 25.0, None, None, None, None, 25.0, 50.0, 33.33]),
              TimeSeries('asPercent(collectd.test-db5.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, None, None, None, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, 25.0, None, None]),
          ]
  
          result = functions.asPercent({}, seriesList)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_integer(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,10)',0,1,1,[10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, 190.0, 200.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,10)',0,1,1,[None, 20.0, None, 40.0, None, 60.0, None, 80.0, None, 100.0, None, 120.0, None, 140.0, None, 160.0, None, 180.0, None, 200.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,10)',0,1,1,[10.0, 20.0, None, None, None, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,10)',0,1,1,[10.0, 20.0, 30.0, 40.0, None, 60.0, None, None, 90.0, 100.0, 110.0, None, 130.0, None, None, None, None, 180.0, 190.0, 200.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,10)',0,1,1,[10.0, 20.0, None, None, None, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, 10)
  
          for i, series in enumerate(result):
              for k, v in enumerate(series):
                if type(v) is float:
                  series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_single(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db1.load.value)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, None, 100.0, None, None, 100.0, 100.0, 100.0, None, 100.0, None, None, None, None, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, seriesList2)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_multi(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db2.load.value)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db3.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,collectd.test-db4.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, None, 100.0, None, None, 100.0, 100.0, 100.0, None, 100.0, None, None, None, None, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,collectd.test-db5.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, seriesList2)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_nodes_total(self):
          self.maxDiff = None
  
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value',
                  'collectd.test-db1.load.avg',
                  'collectd.test-db2.load.avg',
                  'collectd.test-db3.load.avg',
                  'collectd.test-db4.load.avg',
                  'collectd.test-db6.load.avg',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.total',
                  'collectd.test-db2.load.total',
                  'collectd.test-db3.load.total',
                  'collectd.test-db4.load.total',
                  'collectd.test-db5.load.total',
                  'collectd.test-db5.load.total2',
                  'collectd.test-db7.load.total',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.total)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db1.load.avg,collectd.test-db1.load.total)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db2.load.total)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.avg,collectd.test-db2.load.total)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db3.load.total)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db3.load.avg,collectd.test-db3.load.total)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.
  
  [... the rest is omitted ...]
files.new_path: webapp/tests/test_functions.py
files.old_contents: import copy
  import inspect
  import math
  import pytz
  import six
  
  from datetime import datetime
  from fnmatch import fnmatch
  from mock import patch, call, MagicMock
  from os.path import dirname, join
  from six.moves import range
  
  from .base import TestCase
  from django.conf import settings
  
  try:
    from django.urls import reverse
  except ImportError:  # Django < 1.10
    from django.core.urlresolvers import reverse
  
  from graphite.errors import NormalizeEmptyResultError
  from graphite.functions import _SeriesFunctions, loadFunctions
  from graphite.render.datalib import TimeSeries
  from graphite.render import functions
  from graphite.render.evaluator import evaluateTarget
  from graphite.render.grammar import grammar
  from graphite.tags.utils import TaggedSeries
  from graphite.util import json
  
  
  def return_greater(series, value):
      return [i for i in series if i is not None and i > value]
  
  
  def return_less(series, value):
      return [i for i in series if i is not None and i < value]
  
  
  class FunctionsTest(TestCase):
  
      def setUp(self):
        super(FunctionsTest, self).setUp()
        # Display more diff.
        self.maxDiff = 1024
  
      #
      # Test safeSum()
      #
  
      def test_safeSum_None(self):
          with self.assertRaises(TypeError):
              functions.safeSum(None)
  
      def test_safeSum_empty_list(self):
          self.assertEqual(functions.safeSum([]), None)
  
      def test_safeSum_all_numbers(self):
          self.assertEqual(functions.safeSum([1,2,3,4]), 10)
  
      def test_safeSum_all_None(self):
          self.assertEqual(functions.safeSum([None,None,None,None]), None)
  
      def test_safeSum_mixed(self):
          self.assertEqual(functions.safeSum([10,None,5,None]), 15)
  
      #
      # Test safeDiff()
      #
  
      def test_safeDiff_None(self):
          with self.assertRaises(TypeError):
              functions.safeDiff(None)
  
      def test_safeDiff_empty_list(self):
          self.assertEqual(functions.safeDiff([]), None)
  
      def test_safeDiff_all_numbers(self):
          self.assertEqual(functions.safeDiff([1,2,3,4]), -8)
  
      def test_safeDiff_all_None(self):
          self.assertEqual(functions.safeDiff([None,None,None,None]), None)
  
      def test_safeDiff_mixed(self):
          self.assertEqual(functions.safeDiff([10,None,5,None]), 5)
  
      #
      # Test safeLen()
      #
  
      def test_safeLen_None(self):
          with self.assertRaises(TypeError):
              functions.safeLen(None)
  
      def test_safeLen_empty_list(self):
          self.assertEqual(functions.safeLen([]), 0)
  
      def test_safeLen_all_numbers(self):
          self.assertEqual(functions.safeLen([1,2,3,4]), 4)
  
      def test_safeLen_all_None(self):
          self.assertEqual(functions.safeLen([None,None,None,None]), 0)
  
      def test_safeLen_mixed(self):
          self.assertEqual(functions.safeLen([10,None,5,None]), 2)
  
      #
      # Test safeDiv()
      #
  
      def test_safeDiv_None_None(self):
          self.assertEqual(functions.safeDiv(None, None), None)
  
      def test_safeDiv_5_None(self):
          self.assertEqual(functions.safeDiv(5, None), None)
  
      def test_safeDiv_5_0(self):
          self.assertEqual(functions.safeDiv(5, 0), None)
  
      def test_safeDiv_0_10(self):
          self.assertEqual(functions.safeDiv(0,10), 0)
  
      def test_safeDiv_10_5(self):
          self.assertEqual(functions.safeDiv(10,5), 2)
  
      #
      # Test safePow()
      #
  
      def test_safePow_None_None(self):
          self.assertEqual(functions.safePow(None, None), None)
  
      def test_safePow_5_None(self):
          self.assertEqual(functions.safePow(5, None), None)
  
      def test_safePow_5_0(self):
          self.assertEqual(functions.safePow(5, 0), 1.0)
  
      def test_safePow_0_10(self):
          self.assertEqual(functions.safePow(0,10), 0)
  
      def test_safePow_10_5(self):
          self.assertEqual(functions.safePow(10,5), 100000.0)
  
      #
      # Test safeMul()
      #
  
      def test_safeMul_None_None(self):
          self.assertEqual(functions.safeMul(None, None), None)
  
      def test_safeMul_5_None(self):
          self.assertEqual(functions.safeMul(5, None), None)
  
      def test_safeMul_5_0(self):
          self.assertEqual(functions.safeMul(5, 0), 0.0)
  
      def test_safeMul_0_10(self):
          self.assertEqual(functions.safeMul(0,10), 0)
  
      def test_safeMul_10_5(self):
          self.assertEqual(functions.safeMul(10,5), 50.0)
  
      #
      # Test safeSubtract()
      #
  
      def test_safeSubtract_None_None(self):
          self.assertEqual(functions.safeSubtract(None, None), None)
  
      def test_safeSubtract_5_None(self):
          self.assertEqual(functions.safeSubtract(5, None), None)
  
      def test_safeSubtract_5_0(self):
          self.assertEqual(functions.safeSubtract(5, 0), 5.0)
  
      def test_safeSubtract_0_10(self):
          self.assertEqual(functions.safeSubtract(0,10), -10)
  
      def test_safeSubtract_10_5(self):
          self.assertEqual(functions.safeSubtract(10,5), 5)
  
      #
      # Test safeAvg()
      #
  
      def test_safeAvg_None(self):
          with self.assertRaises(TypeError):
              functions.safeAvg(None)
  
      def test_safeAvg_empty_list(self):
          self.assertEqual(functions.safeAvg([]), None)
  
      def test_safeAvg_all_numbers(self):
          self.assertEqual(functions.safeAvg([1,2,3,4]), 2.5)
  
      def test_safeAvg_all_None(self):
          self.assertEqual(functions.safeAvg([None,None,None,None]), None)
  
      def test_safeAvg_mixed(self):
          self.assertEqual(functions.safeAvg([10,None,5,None]), 7.5)
  
      #
      # Test safeMedian()
      #
  
      def test_safeMedian_None(self):
          with self.assertRaises(TypeError):
              functions.safeMedian(None)
  
      def test_safeMedian_empty_list(self):
          self.assertEqual(functions.safeMedian([]), None)
  
      def test_safeMedian_all_numbers_odd_len(self):
          self.assertEqual(functions.safeMedian([1,2,3,4,5]), 3)
  
      def test_safeMedian_all_numbers_even_len(self):
          self.assertAlmostEqual(functions.safeMedian([1,2,3,4]), 2.5)
  
      def test_safeMedian_all_None(self):
          self.assertEqual(functions.safeMedian([None,None,None,None]), None)
  
      def test_safeMedian_mixed(self):
          self.assertAlmostEqual(functions.safeMedian([10,None,5,None]), 7.5)
  
      #
      # Test safeStdDev()
      #
  
      def test_safeStdDev_None(self):
          with self.assertRaises(TypeError):
              functions.safeStdDev(None)
  
      def test_safeStdDev_empty_list(self):
          self.assertEqual(functions.safeStdDev([]), None)
  
      def test_safeStdDev_all_numbers(self):
          self.assertEqual(functions.safeStdDev([1,2,3,4]), 1.118033988749895)
  
      def test_safeStdDev_all_None(self):
          self.assertEqual(functions.safeStdDev([None,None,None,None]), None)
  
      def test_safeStdDev_mixed(self):
          self.assertEqual(functions.safeStdDev([10,None,5,None]), 2.5)
  
      #
      # Test safeLast()
      #
  
      def test_safeLast_None(self):
          with self.assertRaises(TypeError):
              functions.safeLast(None)
  
      def test_safeLast_empty_list(self):
          self.assertEqual(functions.safeLast([]), None)
  
      def test_safeLast_all_numbers(self):
          self.assertEqual(functions.safeLast([1,2,3,4]), 4)
  
      def test_safeLast_all_None(self):
          self.assertEqual(functions.safeLast([None,None,None,None]), None)
  
      def test_safeLast_mixed(self):
          self.assertEqual(functions.safeLast([10,None,5,None]), 5)
  
      #
      # Test safeMin()
      #
  
      def test_safeMin_None(self):
          with self.assertRaises(TypeError):
              functions.safeMin(None)
  
      def test_safeMin_empty_list(self):
          self.assertEqual(functions.safeMin([]), None)
  
      def test_safeMin_all_numbers(self):
          self.assertEqual(functions.safeMin([1,2,3,4]), 1)
  
      def test_safeMin_all_None(self):
          self.assertEqual(functions.safeMin([None,None,None,None]), None)
  
      def test_safeMin_mixed(self):
          self.assertEqual(functions.safeMin([10,None,5,None]), 5)
  
      #
      # Test safeMax()
      #
  
      def test_safeMax_None(self):
          with self.assertRaises(TypeError):
              functions.safeMax(None)
  
      def test_safeMax_empty_list(self):
          self.assertEqual(functions.safeMax([]), None)
  
      def test_safeMax_all_numbers(self):
          self.assertEqual(functions.safeMax([1,2,3,4]), 4)
  
      def test_safeMax_all_None(self):
          self.assertEqual(functions.safeMax([None,None,None,None]), None)
  
      def test_safeMax_mixed(self):
          self.assertEqual(functions.safeMax([10,None,5,None]), 10)
  
      #
      # Test safeAbs()
      #
  
      def test_safeAbs_None(self):
          self.assertEqual(functions.safeAbs(None), None)
  
      def test_safeAbs_empty_list(self):
          with self.assertRaises(TypeError):
            functions.safeAbs([])
  
      def test_safeAbs_pos_number(self):
          self.assertEqual(functions.safeAbs(1), 1)
  
      def test_safeAbs_neg_numbers(self):
          self.assertEqual(functions.safeAbs(-1), 1)
  
      def test_safeAbs_zero(self):
          self.assertEqual(functions.safeAbs(0), 0)
  
      #
      # Test safeMap()
      #
  
      def test_safeMap_None(self):
          with self.assertRaises(TypeError):
              functions.safeMap(abs, None)
  
      def test_safeMap_empty_list(self):
          self.assertEqual(functions.safeMap(abs, []), None)
  
      def test_safeMap_all_numbers(self):
          self.assertEqual(functions.safeMap(abs, [1,2,3,4]), [1,2,3,4])
  
      def test_safeMap_all_None(self):
          self.assertEqual(functions.safeMap(abs, [None,None,None,None]), None)
  
      def test_safeMap_mixed(self):
          self.assertEqual(functions.safeMap(abs, [10,None,5,None]), [10,5])
  
      #
      # Test gcd()
      #
  
      def test_gcd_None_None(self):
          with self.assertRaises(TypeError):
              functions.gcd(None, None)
  
      def test_gcd_5_None(self):
          with self.assertRaises(TypeError):
              functions.gcd(5, None)
  
      def test_gcd_5_0(self):
          self.assertEqual(functions.gcd(5, 0), 5)
  
      def test_gcd_0_10(self):
          self.assertEqual(functions.gcd(0,10), 10)
  
      def test_gcd_10_5(self):
          self.assertEqual(functions.gcd(10,5), 5)
  
      #
      # Test lcm()
      #
  
      def test_lcm_None_None(self):
          self.assertEqual(functions.lcm(None, None), None)
  
      def test_lcm_5_None(self):
          with self.assertRaises(TypeError):
              functions.lcm(5, None)
  
      def test_lcm_5_0(self):
          self.assertEqual(functions.lcm(5, 0), 0)
  
      def test_lcm_0_10(self):
          self.assertEqual(functions.lcm(0,10), 0)
  
      def test_lcm_10_5(self):
          self.assertEqual(functions.lcm(10,5), 10)
  
      #
      # Test xffValues()
      #
  
      def test_xffValues_None(self):
          self.assertEqual(functions.xffValues(None, 0.5), False)
  
      def test_xffValues_empty_list(self):
          self.assertEqual(functions.xffValues([], 0.5), False)
  
      def test_xffValues_all_numbers(self):
          self.assertEqual(functions.xffValues([1,2,3,4], 0.5), True)
  
      def test_xffValues_all_None(self):
          self.assertEqual(functions.xffValues([None,None,None,None], 0.5), False)
  
      def test_xffValues_mixed(self):
          self.assertEqual(functions.xffValues([10,None,5,None], 0.5), True)
  
      #
      # Test xff()
      #
  
      def test_xff_None(self):
          self.assertEqual(functions.xff(None, None, 0.5), False)
  
      def test_xff(self):
          self.assertEqual(functions.xff(1, 2, 0.5), True)
  
      #
      # Test normalize()
      #
  
      def test_normalize_empty(self):
          with self.assertRaises(NormalizeEmptyResultError):
              functions.normalize([])
  
      def test_normalize_None_values(self):
          seriesList = []
          seriesList.append(TimeSeries("collectd.test-db{0}.load.value", 0, 5, 1, [None, None, None, None, None]))
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 5, 1))
  
      def test_normalize_generate_series_list_input(self):
          seriesList = self._generate_series_list()
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 101, 1))
  
      def test_normalize_different_steps(self):
          seriesList = [
            TimeSeries("test.1", 0, 120 * 31, 120, values=list(range(1, 32))),
            TimeSeries("test.2", 0, 30 * 120, 30, values=list(range(1, 121))),
          ]
  
          self.assertEqual(seriesList[0].start, 0)
          self.assertEqual(seriesList[0].end, 120 * 31)
          self.assertEqual(seriesList[0].step, 120)
          self.assertEqual(seriesList[0].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[0])), 31)
  
          self.assertEqual(seriesList[1].start, 0)
          self.assertEqual(seriesList[1].end, 30 * 120)
          self.assertEqual(seriesList[1].step, 30)
          self.assertEqual(seriesList[1].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[1])), 120)
  
          # normalize seriesList
          self.assertEqual(functions.normalize([seriesList]), (seriesList, 0, 3720, 120))
  
          self.assertEqual(seriesList[0].start, 0)
          self.assertEqual(seriesList[0].end, 120 * 31)
          self.assertEqual(seriesList[0].step, 120)
          self.assertEqual(seriesList[0].valuesPerPoint, 1)
          self.assertEqual(len(list(seriesList[0])), 31)
  
          self.assertEqual(seriesList[1].start, 0)
          self.assertEqual(seriesList[1].end, 30 * 120)
          self.assertEqual(seriesList[1].step, 30)
          self.assertEqual(seriesList[1].valuesPerPoint, 4)
          self.assertEqual(len(list(seriesList[1])), 30)
  
      #
      # Test matchSeries()
      #
      def test_matchSeries_assert(self):
          seriesList = self._generate_series_list()
          with self.assertRaisesRegexp(AssertionError, 'The number of series in each argument must be the same'):
              functions.matchSeries(seriesList[0], [])
  
      def test_matchSeries_empty(self):
          results=functions.matchSeries([],[])
          for i, (series1, series2) in enumerate(results):
              self.assertEqual(series1, [])
              self.assertEqual(series2, [])
  
      def test_matchSeries(self):
          seriesList1 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db3.load.value',
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db4.load.value'
              ],
              end=1,
              data=[
                  [3,30,31],
                  [1,10,11],
                  [2,20,21],
                  [4,40,41]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db4.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value'
              ],
              end=1,
              data=[
                  [4,8,12],
                  [3,7,11],
                  [1,5,9],
                  [2,6,10]
              ]
          )
  
          expectedResult = [
          [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[1,10,11]),
              TimeSeries('collectd.test-db2.load.value',0,1,1,[2,20,21]),
              TimeSeries('collectd.test-db3.load.value',0,1,1,[3,30,31]),
              TimeSeries('collectd.test-db4.load.value',0,1,1,[4,40,41]),
          ],
          [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[1,5,9]),
              TimeSeries('collectd.test-db2.load.value',0,1,1,[2,6,10]),
              TimeSeries('collectd.test-db3.load.value',0,1,1,[3,7,11]),
              TimeSeries('collectd.test-db4.load.value',0,1,1,[4,8,12]),
          ]]
          results = functions.matchSeries(copy.deepcopy(seriesList1), copy.deepcopy(seriesList2))
          for i, (series1, series2) in enumerate(results):
              self.assertEqual(series1, expectedResult[0][i])
              self.assertEqual(series2, expectedResult[1][i])
  
      #
      # Test formatPathExpressions()
      #
  
      def test_formatPathExpressions_empty_list(self):
          self.assertEqual(functions.formatPathExpressions([]), '')
  
      def test_formatPathExpressions(self):
          seriesList = self._generate_series_list()
          self.assertEqual(functions.formatPathExpressions(seriesList), "collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value")
  
      #
      # test aggregate()
      #
  
      def test_aggregate_tags(self):
          seriesList = self._gen_series_list_with_data(
              key=['disk.bytes_used;server=server1', 'disk.bytes_free;server=server1'],
              start=0,
              end=3,
              data=[[10, 20, 30], [90, 80, 70]]
          )
  
          result = functions.aggregate({}, seriesList, 'sum')
          self.assertEqual(result, [
              TimeSeries('sumSeries(disk.bytes_used;server=server1,disk.bytes_free;server=server1)', 0, 3, 1, [100, 100, 100]),
          ])
          self.assertEqual(result[0].tags, {
            'name': 'sumSeries(disk.bytes_used;server=server1,disk.bytes_free;server=server1)',
            'server': 'server1',
            'aggregatedBy': 'sum',
          })
  
          seriesList = self._gen_series_list_with_data(
              key=['disk.bytes_used;server=server1', 'disk.bytes_used;server=server2'],
              start=0,
              end=3,
              data=[[10, 20, 30], [90, 80, 70]]
          )
  
          result = functions.aggregate({}, seriesList, 'sum')
          self.assertEqual(result, [
              TimeSeries('sumSeries(disk.bytes_used;server=server1,disk.bytes_used;server=server2)', 0, 3, 1, [100, 100, 100]),
          ])
          self.assertEqual(result[0].tags, {
            'name': 'disk.bytes_used',
            'aggregatedBy': 'sum',
          })
  
      #
      # Test sumSeries()
      #
  
      def test_sumSeries_empty(self):
          self.assertEqual(functions.sumSeries({}, []), [])
  
      def test_sumSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,202,2))
          expected_name = "sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.sumSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_sumSeriesWithWildcards_empty_series_int_position(self):
          self.assertEqual(functions.sumSeriesWithWildcards({}, [], 0), [])
  
      def test_sumSeriesWithWildcards(self):
          seriesList = self._generate_series_list()
          data = list(range(0,202,2))
          expected_name = "load.value"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.sumSeriesWithWildcards({}, [seriesList[0], seriesList[1]], 0,1)
          self.assertEqual(result, expectedList)
  
      def test_averageSeriesWithWildcards_empty_series_int_position(self):
          self.assertEqual(functions.averageSeriesWithWildcards({}, [], 0), [])
  
      def test_averageSeriesWithWildcards(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101,1))
          expected_name = "load.value"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.averageSeriesWithWildcards({}, [seriesList[0], seriesList[1]], 0,1)
          self.assertEqual(result, expectedList)
  
      def test_multiplySeriesWithWildcards(self):
          seriesList1 = self._gen_series_list_with_data(
              key=[
                  'web.host-1.avg-response.value',
                  'web.host-2.avg-response.value',
                  'web.host-3.avg-response.value',
                  'web.host-4.avg-response.value'
              ],
              end=1,
              data=[
                  [1,10,11],
                  [2,20,21],
                  [3,30,31],
                  [4,40,41]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'web.host-4.avg-response.value',
                  'web.host-3.avg-response.value',
                  'web.host-1.avg-response.value',
                  'web.host-2.avg-response.value'
              ],
              end=1,
              data=[
                  [4,8,12],
                  [3,7,11],
                  [1,5,9],
                  [2,6,10]
              ]
          )
  
          expectedResult = [
              TimeSeries('web.host-1',0,1,1,[1,50,99]),
              TimeSeries('web.host-2',0,1,1,[4,120,210]),
              TimeSeries('web.host-3',0,1,1,[9,210,341]),
              TimeSeries('web.host-4',0,1,1,[16,320,492]),
          ]
  
          results = functions.multiplySeriesWithWildcards({}, copy.deepcopy(seriesList1+seriesList2), 2,3)
          self.assertEqual(results,expectedResult)
  
      def test_diffSeries(self):
          seriesList = self._generate_series_list()
          data = [0] * 101
          expected_name = "diffSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.diffSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_aggregate_median(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "medianSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.aggregate({}, [seriesList[0], seriesList[1]], 'median')
          self.assertEqual(result, expectedList)
  
      def test_aggregate_stripSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "medianSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.aggregate({}, [seriesList[0], seriesList[1]], 'medianSeries')
          self.assertEqual(result, expectedList)
  
      def test_aggregate_invalidFunc(self):
          with self.assertRaisesRegexp(Exception, "Unsupported aggregation function: blahSeries"):
              functions.aggregate({}, [], 'blahSeries')
  
      def test_aggregate_emptySeries(self):
          result = functions.aggregate({}, [], 'sum')
          self.assertEqual(result, [])
  
      def test_averageSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "averageSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.averageSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_stddevSeries(self):
          seriesList = self._generate_series_list()
          data = [0.0] * 101
          expected_name = "stddevSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.stddevSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_minSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "minSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.minSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_maxSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "maxSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.maxSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_rangeOfSeries(self):
          seriesList = self._generate_series_list()
          data = [0.0] * 101
          expected_name = "rangeOfSeries(collectd.test-db1.load.value,collectd.test-db2.load.value)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.rangeOfSeries({}, [seriesList[0], seriesList[1]])
          self.assertEqual(result, expectedList)
  
      def test_percentileOfSeries_0th_percentile(self):
          with self.assertRaisesRegexp(ValueError, 'The requested percent is required to be greater than 0'):
              functions.percentileOfSeries({}, [], 0)
  
      def test_percentileOfSeries_empty_series(self):
          series = []
          expected = []
          result = functions.percentileOfSeries({}, series, 90)
          self.assertEqual(result, expected)
  
      def test_percentileOfSeries(self):
          seriesList = self._generate_series_list()
          data = list(range(0,101))
          expected_name = "percentileOfSeries(collectd.test-db1.load.value,90)"
          expectedList = [TimeSeries(expected_name, 0, len(data), 1, data)]
          result = functions.percentileOfSeries({}, [seriesList[0], seriesList[1]], 90)
          self.assertEqual(result, expectedList)
  
      def testGetPercentile_empty_points(self):
          self.assertEqual(functions._getPercentile([], 30), None)
  
      def testGetPercentile_percentile_0(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 15),
              (list(range(100)), 0),
              (list(range(200)), 0),
              (list(range(300)), 0),
              (list(range(1, 101)), 1),
              (list(range(1, 201)), 1),
              (list(range(1, 301)), 1),
              (list(range(0, 102)), 0),
              (list(range(1, 203)), 1),
              (list(range(1, 303)), 1),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 0, True)
              self.assertEqual(expected, result, 'For series index <%s> the 0th percentile ordinal is not %d, but %d ' % (index, expected, result))
  
      def testGetPercentile_interpolated(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 19.0),
              (list(range(100)), 29.3),
              (list(range(200)), 59.3),
              (list(range(300)), 89.3),
              (list(range(1, 101)), 30.3),
              (list(range(1, 201)), 60.3),
              (list(range(1, 301)), 90.3),
              (list(range(0, 102)), 29.9),
              (list(range(1, 203)), 60.9),
              (list(range(1, 303)), 90.9),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 30, True)
              self.assertAlmostEqual(expected, result, 4, 'For series index <%s> the 30th percentile ordinal is not %g, but %g' % (index, expected, result))
  
      def testGetPercentile(self):
          seriesList = [
              ([None, None, 15, 20, 35, 40, 50], 20),
              (list(range(100)), 30),
              (list(range(200)), 60),
              (list(range(300)), 90),
              (list(range(1, 101)), 31),
              (list(range(1, 201)), 61),
              (list(range(1, 301)), 91),
              (list(range(0, 102)), 30),
              (list(range(1, 203)), 61),
              (list(range(1, 303)), 91),
          ]
          for index, conf in enumerate(seriesList):
              series, expected = conf
              result = functions._getPercentile(series, 30)
              self.assertEqual(expected, result, 'For series index <%s> the 30th percentile ordinal is not %d, but %d ' % (index, expected, result))
  
      def test_keepLastValue(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value',
                  'collectd.test-db6.load.value',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,None,3,None,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,None],
              ]
          )
  
          expectedResult = [
              TimeSeries('keepLastValue(collectd.test-db1.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('keepLastValue(collectd.test-db2.load.value)',0,1,1,[None,2,2,4,4,6,6,8,8,10,10,12,12,14,14,16,16,18,18,20]),
              TimeSeries('keepLastValue(collectd.test-db3.load.value)',0,1,1,[1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None]),
              TimeSeries('keepLastValue(collectd.test-db4.load.value)',0,1,1,[1,2,3,4,4,6,6,6,9,10,11,11,13,None,None,None,None,18,19,20]),
              TimeSeries('keepLastValue(collectd.test-db5.load.value)',0,1,1,[1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,18,18]),
              TimeSeries('keepLastValue(collectd.test-db6.load.value)',0,1,1,[1,1,3,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,19]),
          ]
          results = functions.keepLastValue({}, seriesList, 2)
          self.assertEqual(results, expectedResult)
  
      def test_interpolate(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('interpolate(collectd.test-db1.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db2.load.value)',0,1,1,[None,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db3.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None]),
              TimeSeries('interpolate(collectd.test-db4.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),
              TimeSeries('interpolate(collectd.test-db5.load.value)',0,1,1,[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]),
          ]
          results = functions.interpolate({}, seriesList)
          self.assertEqual(results, expectedResult)
  
      def test_changed(self):
          config = [
              [[1,2,3,4,4,5,5,5,6,7], [0,1,1,1,0,1,0,0,1,1]],
              [[None,None,None,None,0,0,0,None,None,1], [0,0,0,0,0,0,0,0,0,1]]
          ]
          for i, c in enumerate(config):
              name = "collectd.test-db{0}.load.value".format(i + 1)
              series = [TimeSeries(name,0,1,1,c[0])]
              expected = [TimeSeries("changed(%s)" % name,0,1,1,c[1])]
              result = functions.changed({}, series)
              self.assertEqual(result, expected)
  
      def test_delay(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(18)) + [None, None]),
          ]
          delay = 2
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,2)',0,1,1,[None, None] + list(range(18))),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_too_many_steps(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(20))),
          ]
          delay = 25
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,25)',0,1,1,[None]*20),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_negative(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,[None,None] + list(range(2,20))),
          ]
          delay = -2
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,-2)',0,1,1,list(range(2,20)) + [None,None]),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_delay_negative_too_many_steps(self):
          source = [
              TimeSeries('collectd.test-db1.load.value',0,1,1,list(range(20))),
          ]
          delay = -25
          expectedList = [
              TimeSeries('delay(collectd.test-db1.load.value,-25)',0,1,1,[None]*20),
          ]
          gotList = functions.delay({}, source, delay)
          self.assertEqual(len(gotList), len(expectedList))
          for got, expected in zip(gotList, expectedList):
              self.assertEqual(got, expected)
  
      def test_asPercent_error(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db1.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
              ]
          )
  
          with self.assertRaisesRegexp(ValueError, "asPercent second argument must be missing, a single digit, reference exactly 1 series or reference the same number of series as the first argument"):
              functions.asPercent({}, seriesList, seriesList2)
  
      def test_asPercent_no_seriesList2(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, 50.0, 33.33, 100.0, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, 25.0, 50.0, 33.33]),
              TimeSeries('asPercent(collectd.test-db2.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[None, 20.0, None, 33.33, None, 20.0, None, 25.0, None, 20.0, None, 25.0, None, 25.0, None, 25.0, None, 25.0, None, 33.33]),
              TimeSeries('asPercent(collectd.test-db3.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, None, None, None, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, 50.0, 33.33, None, 20.0, None, None, 25.0, 20.0, 25.0, None, 25.0, None, None, None, None, 25.0, 50.0, 33.33]),
              TimeSeries('asPercent(collectd.test-db5.load.value,sumSeries(collectd.test-db1.load.value,collectd.test-db2.load.value,collectd.test-db3.load.value,collectd.test-db4.load.value,collectd.test-db5.load.value))',0,1,1,[25.0, 20.0, None, None, None, 20.0, 33.33, 25.0, 25.0, 20.0, 25.0, 25.0, 25.0, 25.0, 33.33, 25.0, 33.33, 25.0, None, None]),
          ]
  
          result = functions.asPercent({}, seriesList)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_integer(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,10)',0,1,1,[10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, 190.0, 200.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,10)',0,1,1,[None, 20.0, None, 40.0, None, 60.0, None, 80.0, None, 100.0, None, 120.0, None, 140.0, None, 160.0, None, 180.0, None, 200.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,10)',0,1,1,[10.0, 20.0, None, None, None, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,10)',0,1,1,[10.0, 20.0, 30.0, 40.0, None, 60.0, None, None, 90.0, 100.0, 110.0, None, 130.0, None, None, None, None, 180.0, 190.0, 200.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,10)',0,1,1,[10.0, 20.0, None, None, None, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, 10)
  
          for i, series in enumerate(result):
              for k, v in enumerate(series):
                if type(v) is float:
                  series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_single(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db1.load.value)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, None, 100.0, None, None, 100.0, 100.0, 100.0, None, 100.0, None, None, None, None, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, seriesList2)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_multi(self):
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value'
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None]
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db2.load.value)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db3.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db4.load.value,collectd.test-db4.load.value)',0,1,1,[100.0, 100.0, 100.0, 100.0, None, 100.0, None, None, 100.0, 100.0, 100.0, None, 100.0, None, None, None, None, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db5.load.value,collectd.test-db5.load.value)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None])
          ]
  
          result = functions.asPercent({}, seriesList, seriesList2)
  
          for i, series in enumerate(result):
            for k, v in enumerate(series):
              if type(v) is float:
                series[k] = round(v,2)
  
          self.assertEqual(result, expectedResult)
  
      def test_asPercent_seriesList2_nodes_total(self):
          self.maxDiff = None
  
          seriesList = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.value',
                  'collectd.test-db2.load.value',
                  'collectd.test-db3.load.value',
                  'collectd.test-db4.load.value',
                  'collectd.test-db5.load.value',
                  'collectd.test-db1.load.avg',
                  'collectd.test-db2.load.avg',
                  'collectd.test-db3.load.avg',
                  'collectd.test-db4.load.avg',
                  'collectd.test-db6.load.avg',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
              ]
          )
  
          seriesList2 = self._gen_series_list_with_data(
              key=[
                  'collectd.test-db1.load.total',
                  'collectd.test-db2.load.total',
                  'collectd.test-db3.load.total',
                  'collectd.test-db4.load.total',
                  'collectd.test-db5.load.total',
                  'collectd.test-db5.load.total2',
                  'collectd.test-db7.load.total',
              ],
              end=1,
              data=[
                  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
                  [None,2,None,4,None,6,None,8,None,10,None,12,None,14,None,16,None,18,None,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,None,None,None],
                  [1,2,3,4,None,6,None,None,9,10,11,None,13,None,None,None,None,18,19,20],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
                  [1,2,None,None,None,6,7,8,9,10,11,12,13,14,15,16,17,18,None,None],
              ]
          )
  
          expectedResult = [
              TimeSeries('asPercent(collectd.test-db1.load.value,collectd.test-db1.load.total)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db1.load.avg,collectd.test-db1.load.total)',0,1,1,[100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.value,collectd.test-db2.load.total)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db2.load.avg,collectd.test-db2.load.total)',0,1,1,[None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0, None, 100.0]),
              TimeSeries('asPercent(collectd.test-db3.load.value,collectd.test-db3.load.total)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, None, None, None]),
              TimeSeries('asPercent(collectd.test-db3.load.avg,collectd.test-db3.load.total)',0,1,1,[100.0, 100.0, None, None, None, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.
  
  [... the rest is omitted ...]
files.old_path: webapp/tests/test_functions.py