Commit 1e6a84b8 authored by bmitta's avatar bmitta

Initial commit

parent 4b60bb9b
/target/
/.classpath
/.project
/.settings/
# shippingapp # shippingapp
Sample springboot application to merge the zipcode ranges
----------------------------------------------------------------------------------------------------------------------
# Problem Statement:
BACKGROUND
Sometimes items cannot be shipped to certain zip codes, and the rules for these restrictions are stored as a series of ranges of 5 digit codes. For example if the ranges are:
[94133,94133] [94200,94299] [94600,94699]
Then the item can be shipped to zip code 94199, 94300, and 65532, but cannot be shipped to 94133, 94650, 94230, 94600, or 94299.
Any item might be restricted based on multiple sets of these ranges obtained from multiple sources.
PROBLEM
Given a collection of 5-digit ZIP code ranges (each range includes both their upper and lower bounds), provide an algorithm that produces the minimum number of ranges required to represent the same restrictions as the input.
NOTES
- The ranges above are just examples, your implementation should work for any set of arbitrary ranges
- Ranges may be provided in arbitrary order
- Ranges may or may not overlap
- Your solution will be evaluated on the correctness and the approach taken, and adherence to coding standards and best practices
EXAMPLES:
If the input = [94133,94133] [94200,94299] [94600,94699]
Then the output should be = [94133,94133] [94200,94299] [94600,94699]
If the input = [94133,94133] [94200,94299] [94226,94399]
Then the output should be = [94133,94133] [94200,94399]
Evaluation Guidelines:
Your work will be evaluated against the following criteria:
- Successful implementation
- Efficiency of the implementation
- Design choices and overall code organization
- Code quality and best practices
---------------------------------------------------------------------------------------------------------------------
# Assumptions made:
1) Assumed input to come as String
For instance input [94133,94133] [94200,94299] [94600,94699] as whole under a string.
# DataStructures used:
- LinkedList - For now, used 2 linkedlist, one for storing the input and the other for storing the output.
- Java8 streams for sorting operation
- Comparator for comparision
# Java File Description:
- RangeService.java --> service class reads the input and drives the zipcode processor
- Range.java --> Data model to store the lower bound and upper bound of zipcode
- InputTransformer.java --> Helps validating the input and load them to linkedlist
- InputProcessor.java --> Main logic that merges the zipcode ranges and returns the final list
- ShippingappApplication.java --> Spring boot main class
# Tests:
1) Wrote Junit tests to validate different scenarios of input
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>shippingapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shippingapp</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.10</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.shipping;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Simple main class for spring boot application
*
*/
@SpringBootApplication
public class ShippingappApplication {
/**
* Simple main method for spring boot application
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(ShippingappApplication.class, args);
}
}
package com.example.shipping.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* This is a simple POJO class
*
*/
@AllArgsConstructor
@Getter
@Setter
@ToString
public class Range {
int min;
int max;
}
package com.example.shipping.processor;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import com.example.shipping.model.Range;
import lombok.extern.slf4j.Slf4j;
/**
* The InputProcessor takes the list of ZIP code ranges and returns list of Range
* objects Checks for overlapping of ranges, and merges if found otherwise keep
* as it is
*
*/
@Slf4j
public class InputProcessor {
/**
* this method takes a list of Ranges, merges overlapping intervals
*
* @param zipRanges
* @return List<Range>
*/
public List<Range> getMinimumRanges(List<Range> zipRanges) {
log.info("processing getMinimumRanges starts.");
// Test if the given list has at least two Ranges
if (zipRanges.size() > 1) {
// Create an empty list for merged Ranges
LinkedList<Range> mergedList = new LinkedList<>();
// sort the Ranges in increasing order of min values
List<Range> sortedList = zipRanges.stream().sorted(Comparator.comparing(Range::getMin))
.collect(Collectors.toList());
// add the first Range to final list/mergedList
mergedList.add(sortedList.get(0));
// Start from the next Range and merge if necessary
for (int i = 1; i < sortedList.size(); i++) {
// get the latest Range from the mergedList
Range lastRecord = mergedList.getLast();
// if current Range is not overlapping with latest range,
// add it to the mergedList
if (lastRecord.getMax() < sortedList.get(i).getMin()) {
mergedList.add(sortedList.get(i));
}
// Otherwise update the max value of latest range, if max of current
// range is more
else if (lastRecord.getMax() < sortedList.get(i).getMax()) {
lastRecord.setMax(sortedList.get(i).getMax());
mergedList.removeLast();
mergedList.add(lastRecord);
}
}
// returning final list
return mergedList;
} else {
return zipRanges;
}
}
}
\ No newline at end of file
package com.example.shipping.service;
import java.util.List;
import com.example.shipping.model.Range;
import com.example.shipping.processor.InputProcessor;
import com.example.shipping.transformer.InputTransformer;
import lombok.extern.slf4j.Slf4j;
/**
*
* The RangeService takes the combination of ZIP code ranges and returns filtered
* ZIP code ranges
*
*/
@Slf4j
public class RangeService {
/**
* This method takes ZIP code ranges as a string and returns the merged response
* if there is any overlap. It uses InputTransformer for transforming the input
* and InputProcessor for merging process
*
* @param zipcodeRangesString
* @return String
*/
public String getMergedZipcodeRanges(String zipcodeRangesString) {
log.info("processing getMergedZipcodeRanges with the input " + zipcodeRangesString);
InputTransformer zipStringtoListTransformer = new InputTransformer(zipcodeRangesString);
List<Range> zipcodeRangeList = zipStringtoListTransformer.transform();
InputProcessor zipcodeRangeProcessor = new InputProcessor();
List<Range> mergedZipcodeRanges = zipcodeRangeProcessor.getMinimumRanges(zipcodeRangeList);
StringBuilder mergedZipcodeRangesStringBuilder = new StringBuilder();
for (Range range : mergedZipcodeRanges) {
mergedZipcodeRangesStringBuilder.append("[" + range.getMin() + "," + range.getMax() + "] ");
}
String resultString = mergedZipcodeRangesStringBuilder.toString().trim();
log.debug("resultString:" + resultString);
log.info("processing getMergedZipcodeRanges completed.");
return resultString;
}
}
package com.example.shipping.transformer;
import java.util.LinkedList;
import java.util.List;
import com.example.shipping.model.Range;
import lombok.extern.slf4j.Slf4j;
/**
* The InputTransformer takes the combination of ZIP code ranges and returns list
* of Range objects
*
*/
@Slf4j
public class InputTransformer {
private String zipcodeRanges;
public InputTransformer(String zipcodeRanges) {
this.zipcodeRanges = zipcodeRanges;
}
/**
* convert the zipcodeRanges strings list of Range objects
*
* @return List<Range>
*/
public List<Range> transform() {
log.info("processing transform starts.");
String[] zipcodeIntervals = zipcodeRanges.split(" ");
return loadZipcode(zipcodeIntervals);
}
/**
* covert ZIP code string to integer
*
* @param zipcode
* @return int
*/
public int stringToInt(String zipcode) {
return Integer.parseInt(zipcode);
}
/**
* check ZIP code length equals to defined limit 5 or not
*
* @param zipcode
* @return boolean
*/
public boolean checkZipLength(int zipcode) {
return ((int) (Math.log10(zipcode) + 1) == 5);
}
/**
* check upper bound is greater than lower bound or not
*
* @param lowerBound
* @param upperBound
* @return boolean
*/
public boolean compareZipcodeRange(int lowerBound, int upperBound) {
return !(lowerBound > upperBound);
}
/**
* validating each ZIP code for length and upper, lower bounds
*
* @param lowerBound
* @param upperBound
* @return
*/
public boolean validateZipcodeRange(int lowerBound, int upperBound) {
if (!checkZipLength(lowerBound) && !checkZipLength(upperBound)) {
log.error(lowerBound + " " + upperBound + ": " + "ZIP code should have 5 digits");
throw new IllegalArgumentException(lowerBound + " " + upperBound + ": " + "ZIP code should have 5 digits");
}
if (!compareZipcodeRange(lowerBound, upperBound)) {
log.error(lowerBound + " " + upperBound + ": " + "ZIP code lower bound should be less than upper bound");
throw new IllegalArgumentException(
lowerBound + " " + upperBound + ": " + "ZIP code lower bound should be less than upper bound");
}
return true;
}
/**
* validate the range
*
* @param zipRange
* @return Range
*/
public Range validateAndAdd(String[] zipRange) {
if (zipRange.length != 2) {
log.error(zipRange[0] + "ZIP code should have lower and upper bounds");
throw new IllegalArgumentException(zipRange[0] + "ZIP code should have lower and upper bounds");
}
int lowerBound = stringToInt(zipRange[0]);
int upperBound = stringToInt(zipRange[1]);
Range zipcode = null;
if (validateZipcodeRange(lowerBound, upperBound))
zipcode = new Range(lowerBound, upperBound);
return zipcode;
}
/**
* @param zipcodeRange
* @return Range
*/
public Range getZipcodeRange(String zipcodeRange) {
return validateAndAdd(zipcodeRange.replaceAll("\\[|\\]", "").split(","));
}
/**
* @param zipcodeRange
* @return
*/
public List<Range> loadZipcode(String[] zipcodeRange) {
List<Range> zipcodesList = new LinkedList<>();
for (int i = 0; i < zipcodeRange.length; i++) {
zipcodesList.add(getZipcodeRange(zipcodeRange[i]));
}
return zipcodesList;
}
}
package com.example.shipping.processor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.LinkedList;
import java.util.List;
import org.junit.jupiter.api.Test;
import com.example.shipping.model.Range;
import com.example.shipping.processor.InputProcessor;
class InputProcessorTest {
@Test
void testOverlappingRangeToReturnOneRange() {
Range zipcode1 = new Range(95000, 95005);
Range zipcode2 = new Range(95002, 95006);
List<Range> zipcodeList = new LinkedList<Range>();
zipcodeList.add(zipcode1);
zipcodeList.add(zipcode2);
InputProcessor zipcode_merger = new InputProcessor();
List<Range> mergedZipcodeList = zipcode_merger.getMinimumRanges(zipcodeList);
assertEquals(1,mergedZipcodeList.size());
assertEquals(95006,mergedZipcodeList.get(0).getMax());
}
@Test
void testLoadAfterCallingMerge() {
Range zipcode1 = new Range(95000, 95005);
Range zipcode2 = new Range(95007, 95008);
List<Range> zipcodeList = new LinkedList<>();
zipcodeList.add(zipcode1);
zipcodeList.add(zipcode2);
InputProcessor zipcode_merger = new InputProcessor();
List<Range> mergedZipcodeList = zipcode_merger.getMinimumRanges(zipcodeList);
assertEquals(mergedZipcodeList, zipcodeList);
}
}
package com.example.shipping.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import com.example.shipping.service.RangeService;
class RangeServiceTest {
@Test
void testfinalResultToMatch() {
String inputDataSet = "[94133,94133] [94200,94299] [94600,94699]";
RangeService zipcodeProcessor = new RangeService();
String output = zipcodeProcessor.getMergedZipcodeRanges(inputDataSet);
assertEquals(inputDataSet, output);
}
@Test
void testfinalResultToMatchWithOverlap() {
String inputDataSet = "[94133,94133] [94200,94299] [94226,94399]";
RangeService zipcodeProcessor = new RangeService();
String output = zipcodeProcessor.getMergedZipcodeRanges(inputDataSet);
assertEquals("[94133,94133] [94200,94399]", output);
}
}
package com.example.shipping.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.example.shipping.transformer.InputTransformer;
class InputTransformerTest {
@Test
void testIllegalArgumentException() {
String inputDataSet = "[92004,92002] [92003,92004]";
InputTransformer transformer = new InputTransformer(inputDataSet);
Assertions.assertThrows(IllegalArgumentException.class, () -> {
transformer.transform();
});
}
@Test
void testExceptionWhenMoreRanges() {
String inputDataSet = "[92004,92002,92003] [92003,92004]";
InputTransformer transformer = new InputTransformer(inputDataSet);
Assertions.assertThrows(IllegalArgumentException.class, () -> {
transformer.transform();
});
}
@Test
void testExceptionMessageWhenLowerBoundGreater() {
String inputDataSet = "[92004,92002] [92003,92004]";
InputTransformer transformer = new InputTransformer(inputDataSet);
Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
transformer.transform();
});
String expectedMessage = "92004 92002: ZIP code lower bound should be less" + " than upper bound";
assertEquals(expectedMessage, exception.getMessage());
}
@Test
void testExceptionMessageWhenMoreRangeGiven() {
String inputDataSet = "[92004,92002,92003] [92003,92004]";
InputTransformer transformer = new InputTransformer(inputDataSet);
Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
transformer.transform();
});
String expectedMessage = "92004ZIP code should have lower " + "and upper bounds";
assertEquals(expectedMessage, exception.getMessage());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment