diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..ab7dafea96 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +### 기능 요구사항 정리 +- [x] 사람의 이름을 갖는다. + - [x] 이름은 최소 1글자 최대 5글자다. +- [x] 라인은 사다리의 가로 한줄을 의미한다. + - [x] 한 라인은 사람수보다 하나 적은 폭을 갖는다. + - [x] 사다리의 경로(path:가로라인)은 연달아 있을 수 없다. (나란히 있을 수 없다.) +- [x] 참여할 사람의 이름은 쉼표로 구분해 받는다. + - [x] 사람은 최소 2명 참가해야한다. +- [x] 사다리의 높이를 입력받는다. + - [x] 최소 높이는 1 이상이다. +- [x] 실행 결과를 출력한다. + - [x] 사다리를 출력한다. + - [x] 라인의 첫 부분은 ` ` 4칸으로 출력한다. + - [x] 5칸과 가장 오른쪽에 `|`를 높이만큼 출력한다. + - [x] 라인에 경로가 있는 경우 `-`를 5칸을 출력한다. + - [x] 라인에 경로가 없는 경우 ` `를 5칸을 출력한다. + - [x] 이름을 출력한다. + - [x] 이름이 4글자 이하인 경우, 오른쪽에 하나의 공백을 두고 우측 정렬한다. + - [x] 이름이 5글자인 경우, 공백없이 우측 정렬한다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..d6152b10f5 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,14 @@ +import controller.LadderGame; +import view.InputView; +import view.OutputView; + +public class Application { + public static void main(String[] args) { + try { + final LadderGame ladderGame = new LadderGame(new InputView(), new OutputView()); + ladderGame.play(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/main/java/controller/LadderGame.java b/src/main/java/controller/LadderGame.java new file mode 100644 index 0000000000..d53d95ee2e --- /dev/null +++ b/src/main/java/controller/LadderGame.java @@ -0,0 +1,38 @@ +package controller; + +import dto.Result; +import java.util.List; +import model.Ladder; +import model.People; +import model.path.RandomPathGenerator; +import view.InputView; +import view.OutputView; + +public class LadderGame { + private final InputView inputView; + private final OutputView outputView; + + public LadderGame(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void play() { + final People people = initPeople(); + final Ladder ladder = initLadder(people.getPersonCount()); + + final Result result = Result.from(people, ladder); + outputView.printResult(result); + } + + + private People initPeople() { + final List names = inputView.inputNames(); + return People.from(names); + } + + private Ladder initLadder(final int personCount) { + final int height = inputView.inputHeight(); + return Ladder.from(height, personCount, new RandomPathGenerator()); + } +} diff --git a/src/main/java/dto/LineInfo.java b/src/main/java/dto/LineInfo.java new file mode 100644 index 0000000000..1b4daf44fb --- /dev/null +++ b/src/main/java/dto/LineInfo.java @@ -0,0 +1,11 @@ +package dto; + +import java.util.List; +import model.Line; + +public record LineInfo(List lineInfo) { + public static LineInfo from(final Line line) { + final List lineInfo = line.getExistFlags(); + return new LineInfo(lineInfo); + } +} diff --git a/src/main/java/dto/Result.java b/src/main/java/dto/Result.java new file mode 100644 index 0000000000..f1de2e77b5 --- /dev/null +++ b/src/main/java/dto/Result.java @@ -0,0 +1,17 @@ +package dto; + +import java.util.List; +import model.Ladder; +import model.People; + +public record Result(List names, List lines) { + public static Result from(final People people, final Ladder ladder) { + final List names = people.getNames(); + final List lines = ladder.getLines() + .stream() + .map(LineInfo::from) + .toList(); + + return new Result(names, lines); + } +} diff --git a/src/main/java/model/Ladder.java b/src/main/java/model/Ladder.java new file mode 100644 index 0000000000..24370199cb --- /dev/null +++ b/src/main/java/model/Ladder.java @@ -0,0 +1,34 @@ +package model; + +import java.util.ArrayList; +import java.util.List; +import model.path.PathGenerator; + +public class Ladder { + private final List lines; + + private Ladder(final List lines) { + this.lines = lines; + } + + public static Ladder from(final int height, final int personCount, final PathGenerator pathGenerator) { + validateHeight(height); + final List lines = new ArrayList<>(); + for (int i = 0; i < height; i++) { + int pathCount = personCount - 1; + final Line line = new Line(pathGenerator.generate(pathCount)); + lines.add(line); + } + return new Ladder(lines); + } + + private static void validateHeight(final int height) { + if (height < 1) { + throw new IllegalArgumentException("사다리의 높이는 1 이상이어야 합니다."); + } + } + + public List getLines() { + return lines; + } +} diff --git a/src/main/java/model/Line.java b/src/main/java/model/Line.java new file mode 100644 index 0000000000..1cbeaaf9a0 --- /dev/null +++ b/src/main/java/model/Line.java @@ -0,0 +1,55 @@ +package model; + +import java.util.List; +import java.util.Objects; +import model.path.Path; + +public class Line { + private static final int MIN_PATH_SIZE = 1; + + private final List paths; + + public Line(final List paths) { + validatePathCount(paths.size()); + checkContinuousPaths(paths); + this.paths = paths; + } + + private void validatePathCount(final int pathSize) { + if (pathSize < MIN_PATH_SIZE) { + throw new IllegalArgumentException("사다리의 경로는 비어있더라도 최소 1개 이상이여야 합니다."); + } + } + + private void checkContinuousPaths(final List paths) { + for (int i = 0; i < paths.size() - 1; i++) { + final Path left = paths.get(i); + final Path right = paths.get(i + 1); + validateContinuousPaths(left, right); + } + } + + private void validateContinuousPaths(final Path left, final Path right) { + if (left == Path.EXIST && right == Path.EXIST) { + throw new IllegalArgumentException("사다리의 경로는 연달아 있을 수 없습니다."); + } + } + + public List getExistFlags() { + return paths.stream() + .map(Path::isExist) + .toList(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Line line = (Line) o; + return Objects.equals(paths, line.paths); + } +} diff --git a/src/main/java/model/People.java b/src/main/java/model/People.java new file mode 100644 index 0000000000..5d30318ce2 --- /dev/null +++ b/src/main/java/model/People.java @@ -0,0 +1,37 @@ +package model; + +import java.util.List; + +public class People { + private static final int MIN_PERSON_COUNT = 2; + + private final List personGroup; + + private People(final List personGroup) { + validatePersonCount(personGroup.size()); + this.personGroup = personGroup; + } + + private void validatePersonCount(final int personCount) { + if (personCount < MIN_PERSON_COUNT) { + throw new IllegalArgumentException("사람은 최소 2명 참가해야 합니다."); + } + } + + public static People from(final List names) { + final List personGroup = names.stream() + .map(Person::new) + .toList(); + return new People(personGroup); + } + + public List getNames() { + return personGroup.stream() + .map(Person::getName) + .toList(); + } + + public int getPersonCount() { + return personGroup.size(); + } +} diff --git a/src/main/java/model/Person.java b/src/main/java/model/Person.java new file mode 100644 index 0000000000..04863b16be --- /dev/null +++ b/src/main/java/model/Person.java @@ -0,0 +1,24 @@ +package model; + +public class Person { + private static final int MIN_NAME_LENGTH = 1; + private static final int MAX_NAME_LENGTH = 5; + + private final String name; + + public Person(final String name) { + validateNameLength(name); + this.name = name; + } + + private void validateNameLength(final String name) { + final int length = name.length(); + if (length < MIN_NAME_LENGTH || length > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름은 최소 1글자 최대 5글자여야 합니다."); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/model/path/FixedPathGenerator.java b/src/main/java/model/path/FixedPathGenerator.java new file mode 100644 index 0000000000..e86c6ea2af --- /dev/null +++ b/src/main/java/model/path/FixedPathGenerator.java @@ -0,0 +1,16 @@ +package model.path; + +import java.util.List; + +public class FixedPathGenerator implements PathGenerator { + private final List lines; + + public FixedPathGenerator(final List lines) { + this.lines = lines; + } + + @Override + public List generate(final int count) { + return this.lines; + } +} diff --git a/src/main/java/model/path/Path.java b/src/main/java/model/path/Path.java new file mode 100644 index 0000000000..b2c7a82ec0 --- /dev/null +++ b/src/main/java/model/path/Path.java @@ -0,0 +1,16 @@ +package model.path; + +public enum Path { + EXIST(true), + NOT_EXIST(false); + + private final boolean isExist; + + Path(final boolean isExist) { + this.isExist = isExist; + } + + public boolean isExist() { + return isExist; + } +} diff --git a/src/main/java/model/path/PathGenerator.java b/src/main/java/model/path/PathGenerator.java new file mode 100644 index 0000000000..2f92ca2da9 --- /dev/null +++ b/src/main/java/model/path/PathGenerator.java @@ -0,0 +1,7 @@ +package model.path; + +import java.util.List; + +public interface PathGenerator { + List generate(final int count); +} diff --git a/src/main/java/model/path/RandomPathGenerator.java b/src/main/java/model/path/RandomPathGenerator.java new file mode 100644 index 0000000000..0d2f26dd31 --- /dev/null +++ b/src/main/java/model/path/RandomPathGenerator.java @@ -0,0 +1,34 @@ +package model.path; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class RandomPathGenerator implements PathGenerator { + private final Random random = new Random(); + + @Override + public List generate(final int count) { + final List paths = new ArrayList<>(); + while (paths.size() < count) { + Path randomPath = getNextPath(paths); + paths.add(randomPath); + } + return paths; + } + + private Path getNextPath(final List paths) { + if (!paths.isEmpty() && getLastPath(paths) == Path.EXIST) { + return Path.NOT_EXIST; + } + if (random.nextBoolean()) { + return Path.EXIST; + } + return Path.NOT_EXIST; + } + + private Path getLastPath(final List paths) { + return paths.get(paths.size() - 1); + } + +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..5f88f54186 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,21 @@ +package view; + +import java.util.List; +import java.util.Scanner; +import view.parser.NameParser; + +public class InputView { + private final Scanner scanner = new Scanner(System.in); + + public List inputNames() { + System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)"); + final String text = scanner.next(); + return NameParser.parse(text); + } + + public int inputHeight() { + System.out.println(); + System.out.println("최대 사다리 높이는 몇 개인가요?"); + return scanner.nextInt(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000000..7e67e09776 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,32 @@ +package view; + +import dto.LineInfo; +import dto.Result; +import view.formatter.LineFormatter; +import view.formatter.NamesFormatter; +import java.util.List; + +public class OutputView { + + public void printResult(final Result result) { + System.out.println(); + System.out.println("실행결과"); + System.out.println(); + final List names = result.names(); + printNames(names); + printLines(result.lines()); + } + + public void printNames(final List names) { + System.out.println(NamesFormatter.format(names)); + } + + public void printLines(final List lines) { + lines.forEach(this::printLine); + } + + private void printLine(final LineInfo line) { + final List paths = line.lineInfo(); + System.out.println(LineFormatter.format(paths)); + } +} diff --git a/src/main/java/view/formatter/LineFormatter.java b/src/main/java/view/formatter/LineFormatter.java new file mode 100644 index 0000000000..c7fb31042e --- /dev/null +++ b/src/main/java/view/formatter/LineFormatter.java @@ -0,0 +1,21 @@ +package view.formatter; + +import java.util.List; + +public class LineFormatter { + + public static String format(final List line) { + String formattedLine = " ".repeat(4) + "|"; + for (Boolean isPath : line) { + formattedLine += formatPath(isPath); + } + return formattedLine; + } + + private static String formatPath(final Boolean isPath) { + if (isPath) { + return "-".repeat(5) + "|"; + } + return " ".repeat(5) + "|"; + } +} diff --git a/src/main/java/view/formatter/NameFormatter.java b/src/main/java/view/formatter/NameFormatter.java new file mode 100644 index 0000000000..50d9b9e097 --- /dev/null +++ b/src/main/java/view/formatter/NameFormatter.java @@ -0,0 +1,11 @@ +package view.formatter; + +public class NameFormatter { + + public static String format(final String name) { + if (name.length() == 5) { + return String.format("%5s", name); + } + return String.format("%4s ", name); + } +} diff --git a/src/main/java/view/formatter/NamesFormatter.java b/src/main/java/view/formatter/NamesFormatter.java new file mode 100644 index 0000000000..d157c66097 --- /dev/null +++ b/src/main/java/view/formatter/NamesFormatter.java @@ -0,0 +1,13 @@ +package view.formatter; + +import java.util.List; + +public class NamesFormatter { + + public static String format(final List names) { + final List formattedNames = names.stream() + .map(NameFormatter::format) + .toList(); + return String.join(" ", formattedNames).stripTrailing(); + } +} diff --git a/src/main/java/view/parser/NameParser.java b/src/main/java/view/parser/NameParser.java new file mode 100644 index 0000000000..3f7bac9a20 --- /dev/null +++ b/src/main/java/view/parser/NameParser.java @@ -0,0 +1,13 @@ +package view.parser; + +import java.util.Arrays; +import java.util.List; + +public class NameParser { + + public static List parse(final String text) { + return Arrays.stream(text.split(",")) + .map(String::trim) + .toList(); + } +} diff --git a/src/test/java/model/LadderTest.java b/src/test/java/model/LadderTest.java new file mode 100644 index 0000000000..de05016c36 --- /dev/null +++ b/src/test/java/model/LadderTest.java @@ -0,0 +1,47 @@ +package model; + +import static model.path.Path.EXIST; +import static model.path.Path.NOT_EXIST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import model.path.FixedPathGenerator; +import model.path.Path; +import model.path.RandomPathGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LadderTest { + + @Test + @DisplayName("사다리를 생성한다.") + void createLadder() { + List expectedPaths = List.of(EXIST, NOT_EXIST); + FixedPathGenerator pathGenerator = new FixedPathGenerator(expectedPaths); + int height = 1; + int personCount = 2; + + Ladder ladder = Ladder.from(height, personCount, pathGenerator); + Line actualLine = ladder.getLines().get(height - 1); + assertThat(actualLine).isEqualTo(new Line(expectedPaths)); + } + + @ParameterizedTest(name = "사다리의 높이는 1 이상이다.") + @ValueSource(ints = {1, 5}) + void createLadder(int height) { + assertThatCode(() -> Ladder.from(height, 4, new RandomPathGenerator())); + } + + @Test + @DisplayName("사다리의 높이는 1 미만은 불가능이다.") + void createLadderThrowException() { + int height = 0; + assertThatThrownBy(() -> Ladder.from(height, 4, new RandomPathGenerator())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사다리의 높이는 1 이상이어야 합니다."); + } +} diff --git a/src/test/java/model/LineTest.java b/src/test/java/model/LineTest.java new file mode 100644 index 0000000000..f707f01644 --- /dev/null +++ b/src/test/java/model/LineTest.java @@ -0,0 +1,28 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import model.path.Path; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LineTest { + + @Test + @DisplayName("참여할 사람은 최소 2명이다.") + void createLineThrowException() { + assertThatThrownBy(() -> new Line(List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사다리의 경로는 비어있더라도 최소 1개 이상이여야 합니다."); + } + + @Test + @DisplayName("사다리의 경로는 연달아 있을 수 없다.") + void createPathThrowException() { + List paths = List.of(Path.EXIST, Path.EXIST, Path.NOT_EXIST); + assertThatThrownBy(() -> new Line(paths)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사다리의 경로는 연달아 있을 수 없습니다."); + } +} diff --git a/src/test/java/model/PeopleTest.java b/src/test/java/model/PeopleTest.java new file mode 100644 index 0000000000..f8d27b4c44 --- /dev/null +++ b/src/test/java/model/PeopleTest.java @@ -0,0 +1,27 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PeopleTest { + + @Test + @DisplayName("사람은 최소 2명 참가해야한다.") + void createPersonGroup() { + List names = List.of("moly", "loky"); + assertThatCode(() -> People.from(names)); + } + + @Test + @DisplayName("사람은 최소 2명 참가해야한다.") + void createPersonGroupThrowException() { + List names = List.of("loky"); + assertThatThrownBy(() -> People.from(names)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사람은 최소 2명 참가해야 합니다."); + } +} diff --git a/src/test/java/model/PersonTest.java b/src/test/java/model/PersonTest.java new file mode 100644 index 0000000000..b54ded45c8 --- /dev/null +++ b/src/test/java/model/PersonTest.java @@ -0,0 +1,28 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PersonTest { + + @DisplayName("사람은 이름을 갖는다.") + @Test + void createPerson() { + String name = "moly"; + Person person = new Person(name); + assertThat(person.getName()).isEqualTo(name); + } + + @ParameterizedTest(name = "이름은 최소 1글자 최대 5글자다.") + @ValueSource(strings = {"", "mollly"}) + void createPersonThrowException(String name) { + assertThatThrownBy(() -> new Person(name)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 최소 1글자 최대 5글자여야 합니다."); + } +} diff --git a/src/test/java/view/formatter/LineFormatterTest.java b/src/test/java/view/formatter/LineFormatterTest.java new file mode 100644 index 0000000000..95901352a5 --- /dev/null +++ b/src/test/java/view/formatter/LineFormatterTest.java @@ -0,0 +1,29 @@ +package view.formatter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import view.formatter.LineFormatter; + +class LineFormatterTest { + private static Stream generateLineTestData() { + return Stream.of( + Arguments.of(Arrays.asList(true, false, true), " |-----| |-----|"), + Arguments.of(Arrays.asList(false, true, false), " | |-----| |"), + Arguments.of(Arrays.asList(true, false, false), " |-----| | |"), + Arguments.of(Arrays.asList(false, false, true), " | | |-----|") + ); + } + + @ParameterizedTest(name = "사다리 라인을 형식에 맞추어 반환한다.") + @MethodSource("generateLineTestData") + void formatLine(List line, String expected) { + String formattedLine = LineFormatter.format(line); + assertThat(formattedLine).isEqualTo(expected); + } +} diff --git a/src/test/java/view/formatter/NameFormatterTest.java b/src/test/java/view/formatter/NameFormatterTest.java new file mode 100644 index 0000000000..0899f39095 --- /dev/null +++ b/src/test/java/view/formatter/NameFormatterTest.java @@ -0,0 +1,22 @@ +package view.formatter; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import view.formatter.NameFormatter; + +class NameFormatterTest { + + @ParameterizedTest(name = "이름을 형식에 맞추어 반환한다.") + @CsvSource({"'pobi', 'pobi '", + "'honux', 'honux'", + "'crong', 'crong'", + "'jk', ' jk '", + "'k', ' k '" + }) + void formatName(String name, String expected) { + String formattedName = NameFormatter.format(name); + assertThat(formattedName).isEqualTo(expected); + } +} diff --git a/src/test/java/view/formatter/NamesFormatterTest.java b/src/test/java/view/formatter/NamesFormatterTest.java new file mode 100644 index 0000000000..baa564d5ee --- /dev/null +++ b/src/test/java/view/formatter/NamesFormatterTest.java @@ -0,0 +1,21 @@ +package view.formatter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import view.formatter.NamesFormatter; + +class NamesFormatterTest { + + @Test + @DisplayName("이름들을 형식에 맞추어 반환한다.") + void formatName() { + List names = List.of( + "pobi", "honux", "crong", "jk" + ); + String formattedNames = NamesFormatter.format(names); + assertThat(formattedNames).isEqualTo("pobi honux crong jk"); + } +} diff --git a/src/test/java/view/parser/NameParserTest.java b/src/test/java/view/parser/NameParserTest.java new file mode 100644 index 0000000000..e5ead1fc6b --- /dev/null +++ b/src/test/java/view/parser/NameParserTest.java @@ -0,0 +1,19 @@ +package view.parser; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import view.parser.NameParser; + +class NameParserTest { + + @Test + @DisplayName("참여할 사람의 수를 입력받아 라인을 만든다.") + void parse() { + String text = "pobi,honux,crong,jk"; + List names = NameParser.parse(text); + assertThat(names).containsExactlyInAnyOrder("pobi", "honux", "crong", "jk"); + } +}