1

I am trying to calculate the average duration for each stage. So in the array below - I should be able to get the average duration for 'test1', which would be 2.

jobs = [
{
    "build_id": 1,
    "stage_executions": [
        {
          "name": "test1"
          "duration": 1,

        },

        {
          "name": "test2"
          "duration": 16408,

        },

        {
          "name": "test3"
          "duration": 16408,

        },
    ]
 },
 {
    "build_id": 2,
    "stage_executions": [
        {
          "name": "test1"
          "duration": 3,

        },

        {
          "name": "test2"
          "duration": 11408,

        },

        {
          "name": "test3"
          "duration": 2408,

        },
    ]
 }
]

My failed attempt:

avgDuration: function(jobs) {
  let durationSum = 0
  for (let item = 0; item < this.jobs.length; item++) {
    for (let i = 0; i < this.jobs[item].stage.length; item++) {
      durationSum += stage.duration
    }
    durationAverage = durationSum/this.jobs[item].stage.length
  }
  return durationAverage

What am I doing wrong? I'm not sure how to accomplish this since the duration is spread out between each job.

UPDATE: This is return a single average for all stages rateher than per stage

<template>
  <div class="stages">
    <h3>
      Average Duration
    </h3>
    <table>
      <tbody>
          <tr v-for="item in durations">
              <td>
              <b>{{ item.average}} {{ item.count }}</b>
              // this returns only 1 average and 177 count instead of 10 
              <br />
            </td>
          </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import { calculateDuration } from "../../helpers/time.js";
import { liveDuration } from "../../helpers/time.js";
import moment from "moment";

export default {
  name: "Stages",
  data() {
    return {
      jobs: [],
      durations: []
    };
  },
  methods: {
    avgDuration: function(jobs) {
      var averageByName = {}; // looks like { 'name': { average: 111, count: 0 }}
      for (var job of jobs) {
        for(var stage of job.stage_execution) {
              if (averageByName[stage.name] == null) { // we need a new object
                averageByName[stage.name] = { average: 0, count: 0 };
              }
              // just name it so its easier to read
              var averageObj = averageByName[stage.name];
              // update count
              averageObj.count += 1;
              // Cumulative moving average
              averageObj.average = averageObj.average + ( (stage.duration - averageObj.average) / averageObj.count );
              console.log(averageObj.count)
            }
          }
      return averageByName
    },
  },
  created() {
    this.JobExecEndpoint =
      process.env.VUE_APP_TEST_URL +
      "/api/v2/jobs/?limit=10";
    fetch(this.JobExecEndpoint)
      .then(response => response.json())
      .then(body => {
        for (let i = 0; i < body.length; i++) {
        this.jobs.push({
          name: body[i].job.name,
          job: body[i].job,
          stage_execution: body[i].stage_executions,
        });
      }
      })
      .then(() => {
        this.$emit("loading", true);
      })
      .then(() => {
        this.durations = this.avgDuration(this.jobs);
      })
      .catch(err => {
        console.log("Error Fetching:", this.JobExecEndpoint, err);
        return { failure: this.JobExecEndpoint, reason: err };
      });
  }
};
</script>
6
  • what do you want with the result? Commented Jul 9, 2019 at 17:58
  • im not sure yet - return it or create a new array with just job names and average durations Commented Jul 9, 2019 at 18:00
  • do you have always the same length of stages? Commented Jul 9, 2019 at 18:07
  • no not always . Commented Jul 9, 2019 at 18:10
  • Noticed in the second for loop you're incrementing item instead of i. I'm guessing that's a typo Commented Jul 9, 2019 at 18:14

3 Answers 3

2

We can do this pretty simply and without overflow from having too many numbers by using a Cumulative moving average and a few loops.

Here is a line the relevant Wikipedia page on Moving Averages and the most relvant formula below.

Moving Average Formula

I will not go into much detail with the above as there are a lot of documents describing this sort of thing. I will however say that the main reason to this over adding all the values together is that there is a far lower chance of overflow and that is why I am using it for this example.

Here is my solution with comments made in code.

var jobs = [ { "build_id": 1, "stage_executions": [ { "name": "test1", "duration": 1, }, { "name": "test2", "duration": 16408, }, { "name": "test3", "duration": 16408, }, ] }, { "build_id": 2, "stage_executions": [ { "name": "test1", "duration": 3, }, { "name": "test2", "duration": 11408, }, { "name": "test3", "duration": 2408, }, ] } ];

var averageByName = {}; // looks like { 'name': { average: 111, count: 0 }}
for (var job of jobs) {
    for(var stage of job.stage_executions) {
        if (averageByName[stage.name] == null) { // we need a new object
            averageByName[stage.name] = { average: 0, count: 0 };
        }
        // just name it so its easier to read
        var averageObj = averageByName[stage.name];
        // update count
        averageObj.count += 1;
        // Cumulative moving average
        averageObj.average = averageObj.average + ( (stage.duration - averageObj.average) / averageObj.count );
    }
}
// print the averages
for(var name in averageByName) {
    console.log(name, averageByName[name].average);
}

Let me know if you have any questions or if anything is unclear.

Sign up to request clarification or add additional context in comments.

8 Comments

this solution seems like the easiest for me to follow, but I'm having trouble trying to turn your example into a function
@benjaminadon what kind of function are you wanting? Maybe the average for a single stage, or would you prefer an object with the averages for all stages?
I was able to turn it into a working function. Now I have an issue where it is calculating the average of ALL stages instead of per stage. I'm sure this is an issue with my jobs data not being structured as I thought. I will accept your answer though since it put me on the right path and has the working code snippet
@benjaminadon if you include more information in your post I may be able to help you more. Try posting your current function and I'll take a look at it
@benjaminadon Please take a look at this fiddle jsfiddle.net/james_wasson/fmku2gz0. I really only changed the part about loading from the server and it works fine. Are you sure your data is structured the same way?
|
0

You could collect the values in an object for each index and map later only the averages.

var jobs = [{ build_id: 1, stage_executions: [{ name: "test1", duration: 1 }, { name: "test2", duration: 16408 }, { name: "test3", duration: 16408 }] }, { build_id: 2, stage_executions: [{ name: "test1", duration: 3 }, { name: "test2", duration: 11408 }, { name: "test3", duration: 2408 }] }],
    averages = jobs
       .reduce((r, { stage_executions }) => {
           stage_executions.forEach(({ duration }, i) => {
               r[i] = r[i] || { sum: 0, count: 0 };
               r[i].sum += duration;
               r[i].avg = r[i].sum / ++r[i].count;
           });
           return r;
       }, []);

console.log(averages.map(({ avg }) => avg));
console.log(averages);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Comments

0

I've used Array.prototype.flatMap to flatten the jobs array into an array of {name:string,duration:number} object. Also, to make more solution a bit more dynamic the function takes in a field argument which returns the average for that specific field.

 
   const jobs = [
  {
    "build_id": 1,
    "stage_executions": [
      {
        "name": "test1",
        "duration": 1,

      },

      {
        "name": "test2",
        "duration": 16408,

      },

      {
        "name": "test3",
        "duration": 16408,

      },
    ]
  },
  {
    "build_id": 2,
    "stage_executions": [
      {
        "name": "test1",
        "duration": 3,

      },

      {
        "name": "test2",
        "duration": 11408,

      },

      {
        "name": "test3",
        "duration": 2408,

      },
    ]
  }
];

const caller = function(jobs, field) {
  const filtered = jobs
    .flatMap((item) => item.stage_executions)
    .filter(item => {
      return item.name === field;
    })
  const total = filtered.reduce((prev, curr) => {
    return prev + curr.duration;
  }, 0)
  return total / filtered.length;
}

console.log(caller(jobs, 'test1'))
console.log(caller(jobs, 'test2'))
console.log(caller(jobs, 'test3'))

In case you get the error flatMap is not a function. You can add this code snippet in your polyfill or at the top of your js file.

Array.prototype.flatMap = function(lambda) {
  return Array.prototype.concat.apply([], this.map(lambda));
};

PS: for demostration, I obtained the flatMap implementation from here

3 Comments

Can I move the protoype.flatMap into the function?
I know what it does but don't really follow how it's being used or where its being called
Array.flatMap was added in ES2019. Depending on the environment you're running the code, Array.flatMap might not be available. So I just added the implementation in case you run into flatMap is not a function error. The code snippet in my answer doesn't need the implementation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.