-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from ducktordanny/feat/second-phase-api
Feat: Second Phase of transportation problem - API side
- Loading branch information
Showing
8 changed files
with
499 additions
and
4 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
apps/backend/src/app/controllers/transport-problem/services/second-phase.service.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { | ||
secondPhaseFirstResultMock, | ||
secondPhaseSecondResultMock, | ||
vogelKordaFirstResultMock, | ||
vogelKordaSecondResultMock, | ||
} from '@opres/shared/data/mocks'; | ||
import {last} from 'lodash'; | ||
|
||
import {SecondPhaseService} from './second-phase.service'; | ||
|
||
describe('SecondPhaseService', () => { | ||
let service: SecondPhaseService; | ||
|
||
beforeEach(() => { | ||
service = new SecondPhaseService(); | ||
}); | ||
|
||
it('should be created', () => | ||
expect(service).toBeInstanceOf(SecondPhaseService)); | ||
|
||
it("should calculate a transport problem's second phase part 1", () => { | ||
const firstPhaseResult = last(vogelKordaFirstResultMock)?.transportation; | ||
const result = service.calculate(firstPhaseResult, 'explanations'); | ||
expect(result).toEqual(secondPhaseFirstResultMock); | ||
}); | ||
|
||
it("should calculate a transport problem's second phase part 2", () => { | ||
const firstPhaseResult = last(vogelKordaSecondResultMock)?.transportation; | ||
const result = service.calculate(firstPhaseResult, 'explanations'); | ||
expect(result).toEqual(secondPhaseSecondResultMock); | ||
}); | ||
}); |
176 changes: 174 additions & 2 deletions
176
apps/backend/src/app/controllers/transport-problem/services/second-phase.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,190 @@ | ||
import {Injectable} from '@nestjs/common'; | ||
|
||
import { | ||
AuxiliaryVariables, | ||
CalculationMode, | ||
SecondPhaseStep, | ||
SelectedCell, | ||
TransportTable, | ||
} from '@opres/shared/types'; | ||
import { | ||
cloneDeep, | ||
findIndex, | ||
first, | ||
forEach, | ||
last, | ||
map, | ||
minBy, | ||
some, | ||
} from 'lodash'; | ||
|
||
@Injectable() | ||
export class SecondPhaseService { | ||
public calculate( | ||
transportTable: TransportTable, | ||
mode: CalculationMode, | ||
): Array<SecondPhaseStep> { | ||
// todo wip | ||
return []; | ||
const process: Array<SecondPhaseStep> = []; | ||
let nextBase: SelectedCell = {x: 0, y: 0}; | ||
do { | ||
const variables = this.getAuxiliaryVariable(transportTable); | ||
nextBase = this.getNextNewBase(transportTable, variables); | ||
if (nextBase?.value !== undefined) { | ||
const circle = this.getCircle(transportTable, nextBase); | ||
process.push({ | ||
transportation: cloneDeep(transportTable), | ||
auxiliaryVariables: variables, | ||
nextBase: cloneDeep(nextBase), | ||
circle, | ||
}); | ||
this.reduceTransportTable(transportTable, cloneDeep(circle)); | ||
continue; | ||
} | ||
process.push({ | ||
transportation: cloneDeep(transportTable), | ||
auxiliaryVariables: variables, | ||
}); | ||
} while (nextBase?.value !== undefined); | ||
return process; | ||
} | ||
|
||
private getAuxiliaryVariable( | ||
transportTable: TransportTable, | ||
): AuxiliaryVariables { | ||
const variables: AuxiliaryVariables = { | ||
x: map(transportTable[0], () => null), | ||
y: map(transportTable, () => null), | ||
}; | ||
const initialRow = findIndex(transportTable, (row) => | ||
some(row, (cell) => !!cell.transported), | ||
); | ||
variables.y[initialRow] = 0; | ||
|
||
const containsNull = (variable) => variable === null; | ||
while (variables.x.some(containsNull) || variables.y.some(containsNull)) { | ||
forEach(transportTable, (row, rowIndex) => | ||
forEach(row, (cell, columnIndex) => { | ||
const currentXVariable = variables.x[columnIndex]; | ||
const currentYVariable = variables.y[rowIndex]; | ||
|
||
if (cell.transported === undefined) return; | ||
if (currentXVariable === null && currentYVariable === null) return; | ||
if (currentXVariable !== null && currentYVariable !== null) return; | ||
|
||
if (currentYVariable === null) | ||
variables.y[rowIndex] = cell.cost - variables.x[columnIndex]; | ||
else if (currentXVariable === null) | ||
variables.x[columnIndex] = cell.cost - variables.y[rowIndex]; | ||
}), | ||
); | ||
} | ||
return variables; | ||
} | ||
|
||
private getNextNewBase( | ||
transportTable: TransportTable, | ||
variables: AuxiliaryVariables, | ||
): SelectedCell { | ||
let newBase: SelectedCell = {value: undefined, x: 0, y: 0}; | ||
forEach(transportTable, (row, rowIndex) => | ||
forEach(row, (cell, columnIndex) => { | ||
if (cell.transported !== undefined) | ||
return (cell.reducedCost = undefined); | ||
const reducedCost = | ||
cell.cost - (variables.x[columnIndex] + variables.y[rowIndex]); | ||
transportTable[rowIndex][columnIndex].reducedCost = reducedCost; | ||
if ( | ||
(newBase?.value === undefined && reducedCost < 0) || | ||
newBase?.value > reducedCost | ||
) | ||
newBase = {value: reducedCost, x: +columnIndex, y: rowIndex}; | ||
}), | ||
); | ||
return newBase; | ||
} | ||
|
||
private getCircle( | ||
transportTable: TransportTable, | ||
nextBase: SelectedCell, | ||
): Array<SelectedCell> { | ||
let baseCells: Array<SelectedCell> = this.getBaseCellPositions( | ||
transportTable, | ||
nextBase, | ||
); | ||
let removedCells = Infinity; | ||
while (removedCells > 0) { | ||
const filteredBaseCells = baseCells.filter((value, _, array) => { | ||
const xIndexes = array.filter((v) => v.x === value.x).length; | ||
const yIndexes = array.filter((v) => v.y === value.y).length; | ||
return xIndexes > 1 && yIndexes > 1; | ||
}); | ||
removedCells = baseCells.length - filteredBaseCells.length; | ||
baseCells = filteredBaseCells; | ||
} | ||
return this.sortCircleCells(baseCells); | ||
} | ||
|
||
private getMaxReducableValueFromCircle(circle: Array<SelectedCell>): number { | ||
return minBy( | ||
circle?.filter((cell, index) => cell?.value !== -1 && index % 2 !== 0), | ||
'value', | ||
)?.value; | ||
} | ||
|
||
private sortCircleCells(circle: Array<SelectedCell>): Array<SelectedCell> { | ||
const comparingIndex = (isX: boolean) => (isX ? 'x' : 'y'); | ||
let isX = true; | ||
const sortedCircle = [circle[0]]; | ||
for (let i = 1; i < circle.length; i++) { | ||
const lastSortedCell = last(sortedCircle); | ||
const baseCell = circle?.find( | ||
(value) => | ||
value?.[comparingIndex(isX)] === | ||
lastSortedCell?.[comparingIndex(isX)] && | ||
value?.[comparingIndex(!isX)] !== | ||
lastSortedCell?.[comparingIndex(!isX)], | ||
); | ||
sortedCircle.push(baseCell); | ||
isX = !isX; | ||
} | ||
return sortedCircle; | ||
} | ||
|
||
private reduceTransportTable( | ||
transportTable: TransportTable, | ||
circle: Array<SelectedCell>, | ||
): void { | ||
const maxReducableValue = this.getMaxReducableValueFromCircle(circle); | ||
first(circle).value = maxReducableValue; | ||
for (let i = 1; i < circle.length; i++) { | ||
if (!circle[i]) continue; | ||
if (i % 2) circle[i].value -= maxReducableValue; | ||
else circle[i].value += maxReducableValue; | ||
} | ||
forEach(circle, (cell) => { | ||
transportTable[cell?.y][cell?.x].transported = | ||
cell?.value === 0 ? undefined : cell?.value; | ||
}); | ||
} | ||
|
||
private getBaseCellPositions( | ||
transportTable: TransportTable, | ||
nextBase?: SelectedCell, | ||
): Array<SelectedCell> { | ||
const baseCellPositions: Array<SelectedCell> = []; | ||
if (nextBase) baseCellPositions.push({...nextBase, value: -1}); | ||
|
||
forEach(transportTable, (row, rowIndex) => | ||
forEach(row, (cell, columnIndex) => { | ||
if (cell.transported !== undefined) | ||
baseCellPositions.push({ | ||
x: +columnIndex, | ||
y: rowIndex, | ||
value: cell.transported, | ||
}); | ||
}), | ||
); | ||
|
||
return baseCellPositions; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
apps/backend/src/app/controllers/transport-problem/utils/base-cells-counter.util.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import {TransportTable} from '@opres/shared/types'; | ||
import {countBy} from 'lodash'; | ||
|
||
import {getColumnUtil} from './get-column.util'; | ||
|
||
export function getBaseCellsInARow( | ||
transportTable: TransportTable, | ||
rowIndex: number, | ||
): number { | ||
return countBy( | ||
transportTable[rowIndex], | ||
(cell) => cell.transported !== undefined, | ||
).true; | ||
} | ||
|
||
export function getBaseCellsInColumn( | ||
transportTable: TransportTable, | ||
columnIndex: number, | ||
): number { | ||
const column = getColumnUtil(transportTable, columnIndex); | ||
return countBy(column, (cell) => cell.transported !== undefined).true; | ||
} |
8 changes: 8 additions & 0 deletions
8
apps/backend/src/app/controllers/transport-problem/utils/get-column.util.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import {Cell, TransportTable} from '@opres/shared/types'; | ||
|
||
export function getColumnUtil( | ||
table: TransportTable, | ||
columnIndex: number, | ||
): Array<Cell> { | ||
return table.map((row) => row[columnIndex]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.