I've been given the following task:
Given N rectangles with edges parallel to axis, calculate the area of the union of all rectangles. The input and output are files specified in program arguments. Input is represented by N lines with 4 numbers separated by spaces, defining 2 opposite vertices of the rectangle. Output file should contain 1 number - the resulting area of the rectangles' union.
Additional constraints:
- \$1 \le N \le 100\$
- \$-10000 \le x1\$, \$y1\$, \$x2\$, \$y2 \le 10000\$
- Memory consumption < 16 MB
- Program parameters should be validated
- Input file format should be validated.
Examples:
Input:
1 1 7 7
Output:
36
Input:
1 1 3 3
2 2 4 4
Output:
7
My solution:
public class Main {
private List<Rectangle> rectangles = new ArrayList<>();
public static void main(String[] args) {
if (args.length != 2) {
throw new IllegalArgumentException("Invalid arguments number\nProgram usage: java Main input output");
}
Main computer = new Main();
long area = computer.computeAreaFromFile(args[0]);
computer.writeAreaToFile(area, args[1]);
}
public long computeAreaFromFile(String inputFileName) {
rectangles.clear();
String line;
try (BufferedReader inputFileReader = new BufferedReader(new FileReader(inputFileName))) {
long area = 0;
while ((line = inputFileReader.readLine()) != null) {
Rectangle rectangle = Rectangle.fromString(line);
area += addRectangleArea(rectangle, false);
}
return area;
} catch (FileNotFoundException e) {
throw new IllegalArgumentException("Input file not found");
} catch (IOException e) {
throw new RuntimeException(e);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
throw new IllegalArgumentException("Input file contains incorrect line");
}
}
private int addRectangleArea(Rectangle newRectangle, boolean isIntersection) {
int result = 0;
boolean hasIntersections = false;
for (Rectangle existingRectangle : rectangles) {
if (!existingRectangle.contains(newRectangle)) {
List<Rectangle> complements = existingRectangle.complementOf(newRectangle);
if (complements.size() > 0) {
hasIntersections = true;
for (Rectangle complement : complements) {
result += addRectangleArea(complement, true);
}
break;
}
}
}
if (!hasIntersections) {
result += newRectangle.area();
}
if (!isIntersection) {
rectangles.add(newRectangle);
}
return result;
}
private void writeAreaToFile(long area, String outputFileName) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFileName))) {
writer.write(String.valueOf(area));
} catch (IOException e) {
throw new RuntimeException("Could not open file " + outputFileName);
}
}
}
class Rectangle {
public final int x1;
public final int y1;
public final int x2;
public final int y2;
public static Rectangle fromString(String input) throws NumberFormatException, IndexOutOfBoundsException {
String[] splitInput = input.split(" ");
if (splitInput.length != 4) {
throw new IndexOutOfBoundsException();
}
return new Rectangle(Integer.valueOf(splitInput[0]),
Integer.valueOf(splitInput[1]),
Integer.valueOf(splitInput[2]),
Integer.valueOf(splitInput[3]));
}
public Rectangle(int x1, int y1, int x2, int y2) {
this.x1 = Math.min(x1, x2);
this.y1 = Math.min(y1, y2);
this.x2 = Math.max(x1, x2);
this.y2 = Math.max(y1, y2);
}
/**
* Finds a relative complement of the specified rectangle.
*
* @param rectangle rectangle to find a complement of.
* @return {@link List} of the rectangles forming the resulting complement.
*/
public List<Rectangle> complementOf(Rectangle rectangle) {
List<Rectangle> intersections = new ArrayList<>();
if (rectangle.x2 > x1 && x2 > rectangle.x1 && rectangle.y2 > y1 && y2 > rectangle.y1) {
if (rectangle.y1 <= y1) {
intersections.add(new Rectangle(rectangle.x1, rectangle.y1, rectangle.x2, y1));
}
if (y2 <= rectangle.y2) {
intersections.add(new Rectangle(rectangle.x1, y2, rectangle.x2, rectangle.y2));
}
if (rectangle.x1 <= x1) {
intersections.add(new Rectangle(rectangle.x1, Math.max(y1, rectangle.y1), x1, Math.min(y2, rectangle.y2)));
}
if (x2 <= rectangle.x2) {
intersections.add(new Rectangle(x2, Math.max(y1, rectangle.y1), rectangle.x2, Math.min(y2, rectangle.y2)));
}
}
return intersections;
}
/**
* Calculates area of this rectangle.
*
* @return area of this rectangle.
*/
public int area() {
return Math.abs((x1 - x2) * (y1 - y2));
}
/**
* Checks if this rectangle contains the specified rectangle.
*
* @param rectangle rectangle to check for.
* @return true if rectangle inside this, false otherwise.
*/
public boolean contains(Rectangle rectangle) {
return x1 <= rectangle.x1 && rectangle.x2 <= x2 && y1 <= rectangle.y1 && rectangle.y2 <= y2;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Rectangle)) {
return false;
}
Rectangle other = (Rectangle) o;
return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
}
@Override
public int hashCode() {
int result = 17;
result = 37 * result + x1;
result = 37 * result + y1;
result = 37 * result + x2;
result = 37 * result + y2;
return result;
}
@Override
public String toString() {
return String.format("Rectangle with x1: %s y1: %s x2: %s y2: %s", x1, y1, x2, y2);
}
}
I have several questions/considerations:
- Is the provided solution correct?
- I assume the complexity of this algorithm is \$O(n^2)\$. Can it be improved?
- I've overridden
toString()
,equals()
andhashCode()
methods, although I've never called them (excepttoString()
while debugging). Is it bad practise to do so? - I feel that comments in javadoc are lame. How can they be improved and are they necessary at all?
computeAreaFromFile()
does 2 things: it reads file content and performs actual calculations. I think this method should be split, however I'm not sure how I could do this.