///|
pub fn weighted_median(data : Array[Double], weights : Array[Double]) -> Double {
  weighted_quantile(data, weights, 0.5)
}

///|
pub fn weighted_lower_quartile(
  data : Array[Double],
  weights : Array[Double],
) -> Double {
  weighted_quantile(data, weights, 0.25)
}

///|
pub fn weighted_upper_quartile(
  data : Array[Double],
  weights : Array[Double],
) -> Double {
  weighted_quantile(data, weights, 0.75)
}

///|
pub fn weighted_iqr(data : Array[Double], weights : Array[Double]) -> Double {
  weighted_upper_quartile(data, weights) -
  weighted_lower_quartile(data, weights)
}

///|
pub fn trimmed_sum(data : Array[Double], trim_percent : Double) -> Double {
  validate_trim(trim_percent)
  if data.length() == 0 {
    return 0.0
  }
  let sorted = copy_and_sort(data)
  let cut = (sorted.length().to_double() * trim_percent).to_int()
  let mut total = 0.0
  for index = cut; index < sorted.length() - cut; index = index + 1 {
    total += sorted[index]
  }
  total
}

///|
pub fn winsorized_sum(data : Array[Double], trim_percent : Double) -> Double {
  sum_values(winsorize(data, trim_percent))
}

///|
pub fn midhinge(data : Array[Double]) -> Double {
  (lower_quartile(data) + upper_quartile(data)) / 2.0
}

///|
pub fn trimean(data : Array[Double]) -> Double {
  (lower_quartile(data) + 2.0 * median(data) + upper_quartile(data)) / 4.0
}

///|
pub fn quantile_skewness(data : Array[Double]) -> Double {
  let denominator = upper_quartile(data) - lower_quartile(data)
  if denominator == 0.0 {
    0.0
  } else {
    (upper_quartile(data) + lower_quartile(data) - 2.0 * median(data)) /
    denominator
  }
}

///|
pub fn bowley_skewness(data : Array[Double]) -> Double {
  quantile_skewness(data)
}

///|
pub fn moors_kurtosis(data : Array[Double]) -> Double {
  let q1 = quantile(data, 0.125)
  let q2 = quantile(data, 0.375)
  let q3 = quantile(data, 0.625)
  let q4 = quantile(data, 0.875)
  let denominator = upper_quartile(data) - lower_quartile(data)
  if denominator == 0.0 {
    0.0
  } else {
    (q4 - q3 + q1 - q2) / denominator
  }
}

///|
pub fn lower_tail_mean(data : Array[Double], probability : Double) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  validate_probability(probability)
  let threshold = quantile(data, probability)
  let selected = []
  for value in data {
    if value <= threshold {
      selected.push(value)
    }
  }
  mean(selected)
}

///|
pub fn upper_tail_mean(data : Array[Double], probability : Double) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  validate_probability(probability)
  let threshold = quantile(data, probability)
  let selected = []
  for value in data {
    if value >= threshold {
      selected.push(value)
    }
  }
  mean(selected)
}

///|
pub fn expected_shortfall(data : Array[Double], probability : Double) -> Double {
  upper_tail_mean(data, probability)
}

///|
pub fn lower_partial_moment(
  data : Array[Double],
  order : Int,
  threshold : Double,
) -> Double {
  if order < 0 {
    abort("order must be non-negative")
  }
  let mut total = 0.0
  let mut count = 0
  for value in data {
    if value < threshold {
      let mut power = 1.0
      let difference = threshold - value
      for exponent = 0; exponent < order; exponent = exponent + 1 {
        power *= difference
      }
      total += power
      count += 1
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
pub fn upper_partial_moment(
  data : Array[Double],
  order : Int,
  threshold : Double,
) -> Double {
  if order < 0 {
    abort("order must be non-negative")
  }
  let mut total = 0.0
  let mut count = 0
  for value in data {
    if value > threshold {
      let mut power = 1.0
      let difference = value - threshold
      for exponent = 0; exponent < order; exponent = exponent + 1 {
        power *= difference
      }
      total += power
      count += 1
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
pub fn tail_count(
  data : Array[Double],
  threshold : Double,
  upper? : Bool = true,
) -> Int {
  let mut count = 0
  for value in data {
    if upper && value >= threshold {
      count += 1
    } else if !upper && value <= threshold {
      count += 1
    }
  }
  count
}

///|
pub fn tail_fraction(
  data : Array[Double],
  threshold : Double,
  upper? : Bool = true,
) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    tail_count(data, threshold, upper~).to_double() / data.length().to_double()
  }
}

///|
pub fn rank_sum(data : Array[Double]) -> Double {
  sum_values(ranks(data))
}

///|
pub fn rank_distance(left : Array[Double], right : Array[Double]) -> Double {
  if left.length() != right.length() {
    return 0.0
  }
  let left_ranks = ranks(left)
  let right_ranks = ranks(right)
  let mut total = 0.0
  for index = 0; index < left.length(); index = index + 1 {
    total += abs_double(left_ranks[index] - right_ranks[index])
  }
  total
}

///|
pub fn pairwise_median_difference(data : Array[Double]) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  let differences = []
  for left in data {
    for right in data {
      differences.push(left - right)
    }
  }
  median(differences)
}

///|
pub fn pairwise_absolute_difference_median(data : Array[Double]) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  let differences = []
  for left = 0; left < data.length(); left = left + 1 {
    for right = left + 1; right < data.length(); right = right + 1 {
      differences.push(abs_double(data[left] - data[right]))
    }
  }
  median(differences)
}

///|
pub fn median_ratio(data : Array[Double], baseline : Double) -> Double {
  if baseline == 0.0 {
    abort("baseline must not be zero")
  }
  median(data) / baseline
}

///|
pub fn relative_iqr(data : Array[Double]) -> Double {
  let center = abs_double(median(data))
  if center == 0.0 {
    0.0
  } else {
    interquartile_range(data) / center
  }
}