Skip to content

Commit

Permalink
Fix #404 : Redesign TableBuilder and fix printing issues (PR #405)
Browse files Browse the repository at this point in the history
* Redesign Table and TableBuilder classes

- Adds new AbstractTableColumn construtor that receives title, subtitle and format
  to make it easier to create columns. Accordingly add such a constructor to subclasses.
- Enables changing format for time, length, ID and PE columns
  in CloudletsTableBuilder.
  This way, the dev can define his/her own default formats upfront.
- Remove the public addColumn methods from Table interface
  and make then protected. Add newColumn methods that just create
  a column, instead of directly adding them to the table.
- When new columns are created, they are added to a temporary list
  before being inserted into the actual table.
  Columns are added to the table just after calling build.
  This way, we can add columns any time and define
  the format for such columns without worring about the order
  of these method calls (as it's expected for a builder class).

* Add setter and getter methods to define formats for CloudletsTableBuilder
* Update CloudletsTableBuilder default precision for time display
* Remove rounding function

* Move createTableColumns() into build method so that columns are just created with the defined format when calling build().
This solves the issue that the variables are not initialized when createTableColumns() is executed.

* Introduces TableBuilderAbstract.addColumn methods for simplicity
* Introduce Table.colCount() for simplicity
* Makes AbstractTableColumn.setTitle to use empty string when null is given.

* Set AbstractTableColumn subtitle default as empty string
* Sets AbstractTableColumn format default as empty string
* Link the column with its table inset the AbstractTable.addColumn()
* Access table attribute directly inside TableBuilderAbstract

* Reinstantiates colsMappings in TableBuilderAbstract instead
of clearing to reduce computational complexity
and avoid concurrency issues.
If column definitions are changed and the build
method is called again, the list of colsMappings
should be cleared before, to add columns
again with new configurations.

* Removed unused methods on TableBuilderAbstract

Signed-off-by: Manoel Campos <manoelcampos@gmail.com>
Co-authored-by: Manoel Campos <manoelcampos@gmail.com>
  • Loading branch information
struffel and manoelcampos committed Nov 24, 2022
1 parent 8aecd9c commit 9232e8e
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 211 deletions.
45 changes: 29 additions & 16 deletions src/main/java/org/cloudsimplus/builders/tables/AbstractTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public List<TableColumn> getColumns() {
return columns;
}

@Override
public int colCount() {
return columns.size();
}

@Override
public String getTitle() {
return title;
Expand Down Expand Up @@ -198,37 +203,45 @@ private void printSubtitleHeaders() {
@Override
public final Table addColumnList(final String... columnTitles) {
for(final String column: columnTitles){
addColumn(column);
newColumn(column);
}
return this;
}

@Override
public final TableColumn addColumn(final String columnTitle) {
return addColumn(getColumns().size(), columnTitle);
public final TableColumn newColumn(final String title, final String subTitle) {
return newColumn(title).setSubTitle(subTitle);
}

@Override
public final TableColumn addColumn(final String columnTitle, final String columnSubTitle) {
return addColumn(columnTitle).setSubTitle(columnSubTitle);
public final TableColumn newColumn(final String title) {
return newColumn(title, "", "");
}

@Override
public TableColumn addColumn(final String columnTitle, final String columnSubTitle, final String format) {
return addColumn(columnTitle, columnSubTitle).setFormat(format);
/**
* Adds a column to the end of the table.
* @param column the column to add
* @return
*/
protected final TableColumn addColumn(final TableColumn column) {
return addColumn(column, columns.size());
}

@Override
public final TableColumn addColumn(final int index, final TableColumn column) {
columns.add(index, column);
/**
* Adds a column at a given position of the table.
*
* @param column the column to add
* @param index the position in the table to add the column
* @return
*/
protected final TableColumn addColumn(final TableColumn column, final int index) {
column.setTable(this);
if(index > colCount())
columns.add(column);
else columns.add(index, column);
return column;
}

@Override
public final TableColumn addColumn(final TableColumn column) {
return addColumn(columns.size(), column);
}

/**
* Sets the {@link PrintStream} used to print the generated table.
* @param printStream the {@link PrintStream} to set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import org.apache.commons.lang3.StringUtils;

import static java.util.Objects.requireNonNullElse;

/**
* A column of a table to be generated using a {@link Table} class.
* @author Manoel Campos da Silva Filho
Expand Down Expand Up @@ -60,24 +62,34 @@ public AbstractTableColumn(final Table table, final String title) {
}

/**
* Creates a column with a specific title and sub-title.
* Creates a column with a specific title, subtitle and format.
* @param title The column title.
* @param subTitle The column sub-title.
* @param subTitle The column subtitle.
* @param format The column format.
*/
public AbstractTableColumn(final String title, final String subTitle) {
this(null, title, subTitle);
public AbstractTableColumn(final String title, final String subTitle, final String format) {
this.title = title;
this.subTitle = subTitle;
this.setFormat(format);
}

/**
* Creates a column with a specific title and sub-title for a given table.
* Creates a column with a specific title and subtitle for a given table.
* @param title The column title.
* @param subTitle The column sub-title.
* @param subTitle The column subtitle.
*/
public AbstractTableColumn(final Table table, final String title, final String subTitle) {
this(title, subTitle);
this.table = table;
this.title = title;
this.setFormat("");
this.subTitle = subTitle;
}

/**
* Creates a column with a specific title and subtitle.
* @param title The column title.
* @param subTitle The column subtitle.
*/
public AbstractTableColumn(final String title, final String subTitle) {
this(title, subTitle, "");
}

/**
Expand All @@ -90,8 +102,8 @@ public String getTitle() {
}

@Override
public AbstractTableColumn setTitle(String title) {
this.title = title;
public AbstractTableColumn setTitle(final String title) {
this.title = requireNonNullElse(title, "");
return this;
}

Expand All @@ -105,8 +117,8 @@ public String getSubTitle() {
}

@Override
public AbstractTableColumn setSubTitle(String subTitle) {
this.subTitle = subTitle;
public AbstractTableColumn setSubTitle(final String subTitle) {
this.subTitle = requireNonNullElse(subTitle, "");
return this;
}

Expand All @@ -122,7 +134,7 @@ public String getFormat() {

@Override
public final AbstractTableColumn setFormat(String format) {
this.format = format;
this.format = requireNonNullElse(format, "");
return this;
}

Expand Down Expand Up @@ -180,12 +192,8 @@ public String generateSubtitleHeader() {
return generateHeader(subTitle);
}

/**
*
* @return The index of the current column into the
* column list of the {@link #getTable() Table}.
*/
protected int getIndex() {
@Override
public int getIndex() {
return table.getColumns().indexOf(this);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

import java.util.List;

import static java.util.Objects.requireNonNull;

/**
* Builds a table for printing simulation results from a list of Cloudlets.
* It defines a set of default columns but new ones can be added
Expand All @@ -41,12 +43,25 @@
* @since CloudSim Plus 1.0
*/
public class CloudletsTableBuilder extends TableBuilderAbstract<Cloudlet> {
private static final String TIME_FORMAT = "%.0f";
public static final String DEF_FORMAT = "%d";

private static final String SECONDS = "Seconds";
private static final String CPU_CORES = "CPU cores";
private static final String ID = "ID";
private static final String MI = "MI";

/** @see #setTimeFormat(String) */
private String timeFormat = "%.1f";

/** @see #setIdFormat(String) */
private String idFormat = DEF_FORMAT;
/** @see #setLengthFormat(String) */
private String lengthFormat = DEF_FORMAT;
/** @see #setPeFormat(String) */
private String peFormat = DEF_FORMAT;

/**
* Instantiates a builder to print the list of Cloudlets using the a
* Instantiates a builder to print the list of Cloudlets using the
* default {@link MarkdownTable}.
* To use a different {@link Table}, check the alternative constructors.
*
Expand All @@ -69,49 +84,85 @@ public CloudletsTableBuilder(final List<? extends Cloudlet> list, final Table ta

@Override
protected void createTableColumns() {
final String ID = "ID";
addColDataFunction(getTable().addColumn("Cloudlet", ID), Identifiable::getId);
addColDataFunction(getTable().addColumn("Status "), cloudlet -> cloudlet.getStatus().name());
addColDataFunction(getTable().addColumn("DC", ID), cloudlet -> cloudlet.getVm().getHost().getDatacenter().getId());
addColDataFunction(getTable().addColumn("Host", ID), cloudlet -> cloudlet.getVm().getHost().getId());
addColDataFunction(getTable().addColumn("Host PEs ", CPU_CORES), cloudlet -> cloudlet.getVm().getHost().getWorkingPesNumber());
addColDataFunction(getTable().addColumn("VM", ID), cloudlet -> cloudlet.getVm().getId());
addColDataFunction(getTable().addColumn("VM PEs ", CPU_CORES), cloudlet -> cloudlet.getVm().getNumberOfPes());
addColDataFunction(getTable().addColumn("CloudletLen", "MI"), Cloudlet::getLength);
addColDataFunction(getTable().addColumn("FinishedLen", "MI"), Cloudlet::getFinishedLengthSoFar);
addColDataFunction(getTable().addColumn("CloudletPEs", CPU_CORES), Cloudlet::getNumberOfPes);

final var col1 = getTable().addColumn("StartTime", SECONDS).setFormat(TIME_FORMAT);
addColDataFunction(col1, Cloudlet::getExecStartTime);

final var col2 = getTable().addColumn("FinishTime", SECONDS).setFormat(TIME_FORMAT);
addColDataFunction(col2, cl -> roundTime(cl, cl.getFinishTime()));

final var col3 = getTable().addColumn("ExecTime", SECONDS).setFormat(TIME_FORMAT);
addColDataFunction(col3, cl -> roundTime(cl, cl.getActualCpuTime()));
addColumn(getTable().newColumn("Cloudlet", ID), Identifiable::getId);

// 1 extra space to ensure proper formatting
addColumn(getTable().newColumn(" Status") , cloudlet -> cloudlet.getStatus().name());

addColumn(getTable().newColumn("DC", ID, idFormat), cloudlet -> cloudlet.getVm().getHost().getDatacenter().getId());

addColumn(getTable().newColumn("Host", ID, idFormat), cloudlet -> cloudlet.getVm().getHost().getId());
addColumn(getTable().newColumn("Host PEs ", CPU_CORES, peFormat), cloudlet -> cloudlet.getVm().getHost().getWorkingPesNumber());

addColumn(getTable().newColumn("VM", ID, idFormat), cloudlet -> cloudlet.getVm().getId());

// 3 extra spaces to ensure proper formatting
addColumn(getTable().newColumn(" VM PEs", CPU_CORES, peFormat), cloudlet -> cloudlet.getVm().getNumberOfPes());
addColumn(getTable().newColumn("CloudletLen", MI, lengthFormat), Cloudlet::getLength);
addColumn(getTable().newColumn("FinishedLen", MI, lengthFormat), Cloudlet::getFinishedLengthSoFar);
addColumn(getTable().newColumn("CloudletPEs", CPU_CORES, peFormat), Cloudlet::getNumberOfPes);
addColumn(getTable().newColumn("StartTime", SECONDS, timeFormat), Cloudlet::getExecStartTime);
addColumn(getTable().newColumn("FinishTime", SECONDS, timeFormat), Cloudlet::getFinishTime);
addColumn(getTable().newColumn("ExecTime", SECONDS, timeFormat), Cloudlet::getActualCpuTime);
}

/**
* Rounds a given time so that decimal places are ignored.
* Sometimes a Cloudlet start at time 0.1 and finish at time 10.1.
* Previously, in such a situation, the finish time was rounded to 11 (Math.ceil),
* giving the wrong idea that the Cloudlet took 11 seconds to finish.
* This method makes some little adjustments to avoid such a precision issue.
*
* @param cloudlet the Cloudlet being printed
* @param time the time to round
* @return
* Gets the format for time columns.
*/
private double roundTime(final Cloudlet cloudlet, final double time) {
public String getTimeFormat() {
return timeFormat;
}

/*If the given time minus the start time is less than 1,
* it means the execution time was less than 1 second.
* This way, it can't be round.*/
if(time - cloudlet.getExecStartTime() < 1){
return time;
}
/**
* Sets the format for time columns.
*/
public CloudletsTableBuilder setTimeFormat(final String timeFormat) {
this.timeFormat = requireNonNull(timeFormat);
return this;
}

/**
* Gets the format for cloudlet length columns.
*/
public String getLengthFormat() {
return lengthFormat;
}

final double startFraction = cloudlet.getExecStartTime() - (int) cloudlet.getExecStartTime();
return Math.round(time - startFraction);
/**
* Sets the format for cloudlet length columns.
*/
public CloudletsTableBuilder setLengthFormat(final String lengthFormat) {
this.lengthFormat = requireNonNull(lengthFormat);
return this;
}

/**
* Gets the format for ID columns.
*/
public String getIdFormat() {
return idFormat;
}

/**
* Sets the format for ID columns.
*/
public CloudletsTableBuilder setIdFormat(final String idFormat) {
this.idFormat = requireNonNull(idFormat);
return this;
}

/**
* Gets the format for columns indicating number of PEs.
*/
public String getPeFormat() {
return peFormat;
}

/**
* Sets the format for columns indicating number of PEs.
*/
public CloudletsTableBuilder setPeFormat(final String peFormat) {
this.peFormat = requireNonNull(peFormat);
return this;
}
}
31 changes: 31 additions & 0 deletions src/main/java/org/cloudsimplus/builders/tables/ColumnMapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.cloudsimplus.builders.tables;

import java.util.function.Function;

import static java.util.Objects.requireNonNull;

/**
* A record that creates a mapping for adding a column into a table latter on.
* That is used by {@link TableBuilderAbstract} objects.
*
* @param <T> the type of objects printed into the table
* @param col the column to add
* @param dataFunction a function that receives an object T and returns the data to be printed from that object
* @param index the index of the column in the table
* @author Manoel Campos da Silva Filho
* @since CloudSim Plus 7.3.1
*/
record ColumnMapping<T>(TableColumn col, Function<T, Object> dataFunction, int index) {
ColumnMapping(TableColumn col, Function<T, Object> dataFunction) {
this(col, dataFunction, Integer.MAX_VALUE);
}

ColumnMapping {
requireNonNull(col);
requireNonNull(dataFunction);
}

public Object getColData(T object){
return dataFunction.apply(object);
}
}
6 changes: 2 additions & 4 deletions src/main/java/org/cloudsimplus/builders/tables/CsvTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ public String getLineSeparator() {
}

@Override
public TableColumn addColumn(int index, String columnTitle) {
final TableColumn col = new CsvTableColumn(this, columnTitle);
getColumns().add(index, col);
return col;
public TableColumn newColumn(final String title, final String subtitle, final String format) {
return new CsvTableColumn(title, subtitle, format);
}
}

0 comments on commit 9232e8e

Please sign in to comment.