Compare commits

...

2 Commits

2 changed files with 355 additions and 359 deletions

View File

@ -19,6 +19,7 @@ dependencies {
implementation("org.slf4j:slf4j-nop:1.7.29")
implementation("org.jetbrains.lets-plot:lets-plot-image-export:3.1.0")
testImplementation 'org.jetbrains.kotlin:kotlin-test'
implementation 'com.google.code.gson:gson:2.8.9'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

View File

@ -1,55 +1,28 @@
@file:OptIn(ExperimentalCli::class)
import java.io.File
import kotlin.system.exitProcess
import org.jetbrains.letsPlot.*
import org.jetbrains.letsPlot.export.ggsave
import org.jetbrains.letsPlot.geom.*
import kotlinx.cli.*
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.io.FileReader
/** Recognizes the name of the text, which
* user want to see statistics about and type of output.
* It calls methods of graphic building or printing data in console,
* build required for it objects.
private val DELIMITERS = Regex("[!?.]+\\s+")
private val WHITESPACES = Regex("\\s+")
private val WHITESPACES_OR_EMPTY = Regex("(\\s+)?")
/**
* Builds bar chart with data of listOfSentenceSizes and saves image in file.
* @param textName name of text, which user want to see statistics about.
* @param listOfSentenceSizes list of sentences words count.
*/
fun getStatistics(textData: TextData) {
if (!textData.haveText()) {
println("No saved texts")
return
}
val text = textData.getTextData("Input name of a text which you want to see statistics about.")
var counter = 1
val wordsCountsMap = text.getSentencesList().map { counter++ to it.getWordsCount() }
println(
"Print \"console\" if you have see data in console, \"graphic\" if you have see histogram \n" +
"and \"both\" if you have see them together:"
)
val request = requestInput(listOf("console", "graphic", "both"))
request.first.exe()
when (request.second) {
"console" -> printStatisticsInConsole(text.getName(), wordsCountsMap.toMap())
"graphic" -> buildGraphic(text.getName(), wordsCountsMap.toMap())
"both" -> {
printStatisticsInConsole(text.getName(), wordsCountsMap.toMap())
buildGraphic(text.getName(), wordsCountsMap.toMap())
}
}
}
/** Builds bar chart with data of mapOfSentenceNumToItsSize and saves image in file.
* @param textName name of texts, which user want to see statistics about.
* @param mapOfSentenceNumToItsSize map of pairs of sentence numbers and their words count.
*/
fun buildGraphic(textName: String, mapOfSentenceNumToItsSize: Map<Int, Int>) {
val data = mapOf(
"words count" to mapOfSentenceNumToItsSize.map { it.value.toFloat() / mapOfSentenceNumToItsSize.size },
)
fun buildGraphic(textName: String, listOfSentenceSizes: List<Int>) {
val data = mapOf("words count" to listOfSentenceSizes.map { it.toFloat() })
val fig = ggplot(data) +
geomBar(
@ -66,10 +39,10 @@ fun buildGraphic(textName: String, mapOfSentenceNumToItsSize: Map<Int, Int>) {
println("Graphic was save in ${ggsave(fig, "$textName.png")}")
fig.show()
}
/** Prints statistics according to data from mapOfSentenceNumToItsSize.
/**
* Prints statistics according to data from mapOfSentenceNumToItsSize.
* @param textName name of texts, which user want to see statistics about.
* @param mapOfSentenceNumToItsSize map of pairs of sentence numbers and their words count.
*/
@ -99,38 +72,15 @@ fun printStatisticsInConsole(textName: String, mapOfSentenceNumToItsSize: Map<In
""".trimMargin()
)
println("-".repeat(sectionTitle.length))
println("Done!\n")
}
/** Asks the user which where they want to read the text from and
* calls special for file- and console- reading methods according the answer.
/**
* Read from console the text and calls function of adding new text to data.
* @param textData TextData object to add new text to.
* @param textName name of new text.
*/
fun readNewText(textData: TextData) {
println("Where do you want to add the text: from the console or a file?")
val kindOfSource = requestInput(listOf("console", "file"))
kindOfSource.first.exe()
when (kindOfSource.second) {
"console" -> readFromConsole(textData)
"file" -> readFromFile(textData)
}
}
/** Read from console the name of text, checks that it hasn't saved yet and its contents,
* asks if entered text is not correct and re-calls itself or
* calls method of adding received text to data.
*/
fun readFromConsole(textData: TextData) {
println("Input name of a text:")
if (textData.haveText()) println("Unavailable(existing) names: ${textData.getTextNamesInString()}.")
val nameRequest = requestInput(unavailableInputs = textData.getTextNamesList())
nameRequest.first.exe()
val name = nameRequest.second!!
fun readFromConsoleAndAddToData(textData: TextData, textName: String) {
println("Input a text content(after input text press enter twice):")
var content = ""
@ -150,172 +100,169 @@ fun readFromConsole(textData: TextData) {
}
}
println("Was input data right?[yes, no]:")
val correctInputRequest = requestInput(listOf("yes", "no"))
correctInputRequest.first.exe()
if (correctInputRequest.second == "yes") addTextToData(textData, name, content)
else readFromConsole(textData)
}
/** Asks for the name of text, path to file to read text from,
* checks for its existing, reads contents and,
* asks if entered names is not correct and re-calls itself or
* calls method of adding received text to data.
*/
fun readFromFile(textData: TextData) {
println("Input a name of a text:")
val name = readln()
println("Input path to file:")
val contentsFile: File
while (true) {
val pathRequest = requestInput()
pathRequest.first.exe()
val filePath = pathRequest.second!!
val testFile = File(filePath)
if (!testFile.exists()) {
println("Incorrect path. Repeat the input:")
continue
} else {
contentsFile = testFile
break
}
}
val content = contentsFile.readText()
print("Input was correct?[yes, no]: ")
val correctInputRequest = requestInput(listOf("yes", "no"))
correctInputRequest.first.exe()
when (correctInputRequest.second) {
"yes" -> addTextToData(textData, name, content)
"no" -> readFromFile(textData)
}
addTextToData(textData, textName, content)
}
/**
* Calls method of TextData class to add new text in set
* of tracked texts.
* @param textName name of new text.
* @param content content of new text.
* Read text from file and calls function of adding text to data.
* @param textData TextData object to add new text to.
* @param textName name of new text.
* @param path path to the text file.
*/
fun readFromFileAndAddToData(textData: TextData, textName: String, path: String) {
val contentsFile = File(path)
val content = contentsFile.readText()
addTextToData(textData, textName, content)
}
/**
* Calls method of TextData class to add new text in set
* of tracked texts.
* @param textData TextData object to add new text to.
* @param textName name of new text.
* @param content content of new text.
*/
fun addTextToData(textData: TextData, textName: String, content: String) =
textData.addNewText(textName, content)
/** Stores data about tracking texts,
/**
* Stores data about tracking texts,
* includes methods for work with them due the adding.
*/
class TextData {
/** list of monitored texts. */
private val textsList = mutableListOf<Text>()
/** List of monitored texts. */
private val textsList: MutableSet<Text>
fun haveText(): Boolean = textsList.isNotEmpty()
/** Method for removing text from watch list. Asks for a name of a text
* and if it's being tracked, remove it from the textsList.
*/
fun removeText() {
if (!haveText()) {
println("No saved texts")
return
}
val removingText = getTextData("Input name of text which you want to remove.")
textsList.remove(removingText)
println("Text ${removingText.getName()} removed.")
}
/** Returns Text object whose name matches searchingName or null
* if there is no text with same name in the textsList.
* @param searchingName name of the text to be found.
* @return the text with searchingName name or null.
*/
private fun getTextByName(searchingName: String): Text? {
for (text in textsList) if (text.getName() == searchingName) return text
return null
}
/** @return string with names of tracking texts separated by delimiter. */
fun getTextNamesInString(delimiter: String = ", "): String {
return textsList.joinToString(delimiter) { it.getName() }
}
/** @return list with names of tracking texts. */
fun getTextNamesList(): List<String> {
return textsList.map { it.getName() }
}
/** Calls method of textAnalyzer for getting Text object from
* textName and content and adds it in textsList.
*/
fun addNewText(textName: String, content: String) {
textsList.add(TextAnalyzer.getTextObjFromContents(textName, content))
init {
// Initialization of previously saved and saved in JSON file texts
textsList = JSONReaderWriter.readSavedTexts()
}
/**
* Reads name of the text to be found and returns it text if
* it exists.
* @param message message which prints with calling this method.
* @return Text type object
* Removes text with name from watch list if it was there previously.
* @param name the name of text to be deleted.
*/
fun getTextData(message: String = "Input name of a text"): Text {
println(message)
println("Saved texts: ${getTextNamesInString()}.")
fun removeText(name: String) {
val nameRequest = requestInput(getTextNamesList())
nameRequest.first.exe()
val removingText = getTextByName(name)
if (removingText == null) println("No text with name $name")
else {
textsList.remove(removingText)
println("Text ${removingText.getName()} removed.")
JSONReaderWriter.removeFromDirectory(name)
}
}
/**
* Calls method of textAnalyzer for getting Text object from
* textName and content, adds it in textsList and calls method of writing it in JSON.
* @param textName name of text to be added.
* @param content content of text.
*/
fun addNewText(textName: String, content: String) {
val newText = getTextObjFromContents(similarAvailableName(textName), content)
textsList.add(newText)
JSONReaderWriter.writeInJSON(newText)
}
/**
* If exists text in textList with name textName, chooses similar to it name with addition "(*)".
* @param textName the name to check for a match.
* @return name similar to textName.
*/
private fun similarAvailableName(textName: String): String {
val existingNames = textsList.map { it.getName() }
if (textName !in existingNames) return textName
val nameRegex = Regex("$textName\\(\\d+\\)")
val busyNumbers = mutableSetOf<Int>()
for (name in existingNames) {
if (name.matches(nameRegex))
busyNumbers.add(name.substring(name.indexOfLast { it == '(' }, name.indexOfFirst { it == ')' }).toInt())
}
var minimalAvailable = 1
while (true) {
if (minimalAvailable in busyNumbers) minimalAvailable++
else break
}
return "$textName($minimalAvailable)"
return getTextByName(nameRequest.second!!)!!
}
/** Object used for getting text from the string information. */
private object TextAnalyzer {
/**
* Returns Text object from textList whose name matches searchingName or null
* if there is no text with same name in the textsList.
* @param searchingName name of the text to be found.
* @return the text with searchingName name or null.
*/
fun getTextByName(searchingName: String): Text? {
textsList.forEach { if (it.getName() == searchingName) return it }
return null
}
private val DELIMITERS = Regex("[!?.]+\\s+")
private val WHITESPACES = Regex("\\s+")
private val WHITESPACES_OR_EMPTY = Regex("(\\s+)?")
/** @return string with names of tracking texts separated by delimiter. */
fun getTextNamesInString(delimiter: String = ", "): String =
textsList.joinToString(delimiter) { it.getName() }
/**
* Receives text as input and splits it by sentence, calculate its lengths,
* create list of Sentence objects and returns Text object, created with
* this list, input field name, and count of sentences in content.
*/
fun getTextObjFromContents(name: String, content: String): Text {
/** @return list with names of tracking texts. */
fun getTextNamesList(): List<String> = textsList.map { it.getName() }
var sentencesCount = 1
val listOfSentences = mutableListOf<Text.Sentence>()
val sentencesStringList = content
.split(DELIMITERS)
.map { it.replace(WHITESPACES, " ") }.toMutableList()
sentencesStringList.removeIf { it.matches(WHITESPACES_OR_EMPTY) }
/** Uses for writing and reading Text objects in JSON. */
private object JSONReaderWriter {
private val gsonPretty: Gson = GsonBuilder().setPrettyPrinting().create()
private val savedTextsDirectory: File = File("savedTexts")
private val savedTextsFiles: Array<File>
for (sentenceText in sentencesStringList) {
val wordsList = sentenceText.split(WHITESPACES).toMutableList()
wordsList.removeIf { it.matches(WHITESPACES_OR_EMPTY) }
sentencesCount++
listOfSentences.add(Text.Sentence(wordsList.size))
}
return Text(name, sentencesCount, listOfSentences.toList())
init {
// initialization or creation directory with JSON files where Text objects were written.
if (!savedTextsDirectory.exists()) savedTextsDirectory.mkdir()
savedTextsFiles = savedTextsDirectory.listFiles { _, filename ->
filename.split(".").last() == "json"
} ?: emptyArray()
}
/**
* Removes file with name from directory with saved texts.
* @param name name of file to be deleted.
*/
fun removeFromDirectory(name: String) =
savedTextsFiles.find { it.name == name }?.delete() ?: println("No file with name $name in directory")
/**
* Writes in JSON file text.
* @param text Text object to be written.
*/
fun writeInJSON(text: Text) {
val jsonFileText = gsonPretty.toJson(text)
val jsonFile = File("${savedTextsDirectory.path}/${text.getName()}.json")
jsonFile.createNewFile()
jsonFile.writeText(jsonFileText)
}
/**
* @param jsonFile file to read from text object.
* @return Text object read from JSON file.
*/
fun readFromJSON(jsonFile: File): Text = gsonPretty.fromJson(FileReader(jsonFile), Text::class.java)
/**
* Calls method of reading from JSON for each json file from directory with saved texts.
* @return set of Text objects
*/
fun readSavedTexts(): MutableSet<Text> {
val textsList = mutableListOf<Text>()
for (file in savedTextsFiles) textsList.add(readFromJSON(file))
return textsList.toMutableSet()
}
}
@ -325,21 +272,213 @@ class TextData {
private val sentencesCount: Int,
private val sentencesList: List<Sentence>,
) {
fun getName() = name
fun getSentencesList() = sentencesList
/** Stores information about count of words. */
data class Sentence(private val wordsCount: Int) {
fun getWordsCount() = wordsCount
}
}
}
/**
* Receives text as input and splits it by sentence, calculate its lengths,
* create list of Sentence objects and returns Text object, created with
* this list, input field name, and count of sentences in content.
* @param name name of text.
* @param content string content of text.
* @return Text object
*/
private fun TextData.getTextObjFromContents(name: String, content: String): TextData.Text {
var sentencesCount = 1
val listOfSentences = mutableListOf<TextData.Text.Sentence>()
val sentencesStringList = content
.split(DELIMITERS)
.map { it.replace(WHITESPACES, " ") }.toMutableList()
sentencesStringList.removeIf { it.matches(WHITESPACES_OR_EMPTY) }
for (sentenceText in sentencesStringList) {
val wordsList = sentenceText.split(WHITESPACES).toMutableList()
wordsList.removeIf { it.matches(WHITESPACES_OR_EMPTY) }
sentencesCount++
listOfSentences.add(TextData.Text.Sentence(wordsList.size))
}
return TextData.Text(name, sentencesCount, listOfSentences.toList())
}
/**
* Parses list of Strings in commands and its arguments,
* checks the correctness of the entered data and calls related functions.
*/
class CommandCenter(private val textData: TextData) {
/**
* ArgParser object which can distribute arguments
* over declared options and subcommands.
*/
private val parser = ArgParser(
"Text Analyzer",
useDefaultHelpShortName = true,
strictSubcommandOptionsOrder = false
)
/** Subcommands initialization */
private val addNewText = Add()
private val showStatistics = ShowStatistics()
private val showTextList = ShowTextsList()
private val removeTexts = RemoveTexts()
/** Calls function of removing texts from data. */
private inner class RemoveTexts : Subcommand("remove", "Removing text from saved.") {
private val removingList by argument(
ArgType.String, "removing list",
"Selection of texts to delete"
).vararg()
override fun execute() {
if (removingList.isEmpty()) {
println("remove command needs arguments to work")
return
}
val availableRemovingList = mutableListOf<String>()
removingList.forEach {
if (it in textData.getTextNamesList()) availableRemovingList.add(it)
else println("No text $it in data")
}
availableRemovingList.forEach { textData.removeText(it) }
}
}
/** Prints list of saved texts in console. */
private inner class ShowTextsList : Subcommand("list", "Showing tracking texts.") {
private fun printTextListInConsole() = println("Saved texts list: ${textData.getTextNamesInString(", ")}")
override fun execute() {
printTextListInConsole()
}
}
/** Calls functions of graphic building or showing statistics in console. */
private inner class ShowStatistics : Subcommand("stat", "Showing statistics") {
private val textNames by argument(
ArgType.String, "text names",
"Names of texts which you want to show statistics about"
).vararg()
private val outputLocation by option(
ArgType.Choice(listOf("graphic", "console", "both"), { it }), "output-location", "o",
"Choice where to show the statistics"
).default("console")
private fun selectAvailableInputFromInputted(): List<String> {
val availableInputs = mutableListOf<String>()
textNames.forEach {
if (it in textData.getTextNamesList()) availableInputs.add(it)
else println("No text $it in data")
}
return availableInputs
}
private fun callStatisticsShowingForEach(textNames: List<String>) {
when (outputLocation) {
"graphic" -> textNames.forEach { callBuildingGraphic(it) }
"console" -> textNames.forEach { callConsoleStatisticsShowing(it) }
"both" -> textNames.forEach {
callBuildingGraphic(it)
callConsoleStatisticsShowing(it)
}
}
}
private fun callBuildingGraphic(textName: String) {
val text = textData.getTextByName(textName)!!
val listOfSentenceSizes = text.getSentencesList().map { it.getWordsCount() }
buildGraphic(textName, listOfSentenceSizes)
}
private fun callConsoleStatisticsShowing(textName: String) {
val text = textData.getTextByName(textName)!!
var counter = 1
val mapOfSentenceNumToItsSize = text.getSentencesList().associate { counter++ to it.getWordsCount() }
printStatisticsInConsole(textName, mapOfSentenceNumToItsSize)
}
override fun execute() {
val availableSelectedTextNames = selectAvailableInputFromInputted()
callStatisticsShowingForEach(availableSelectedTextNames)
}
}
/** Calls functions of reading and adding text in saved. */
private inner class Add : Subcommand("add", "Adding text from source") {
private val path by option(
ArgType.String, "source path", "s",
"Inputting path to file for reading or \"console\" to reading from console"
).default("console")
private val textName by argument(
ArgType.String, "name",
"Specifies the name of the new text"
)
private fun callReading() = when {
path == "console" -> callReadingFromConsole()
!File(path).exists() -> throw IncorrectPathException(path)
else -> callReadingFromFile()
}
private fun callReadingFromConsole() = readFromConsoleAndAddToData(textData, textName)
private fun callReadingFromFile() = readFromFileAndAddToData(textData, textName, path)
override fun execute() = callReading()
}
/** Calls parse function of ArgParser object. */
fun parseArgsAndExecute(args: Array<String>) = parser.parse(args)
/** Calls initialization of subcommands. */
fun subcommandsInit() = parser.subcommands(addNewText, showStatistics, showTextList, removeTexts)
}
/** Calls parsing of main args or if it is empty, read them from console */
fun readAndCallParsing(commandCenter: CommandCenter, mainArgs: Array<String>) {
if (mainArgs.isNotEmpty()) commandCenter.parseArgsAndExecute(mainArgs)
else {
val args = readln().split(WHITESPACES).toTypedArray()
try {
commandCenter.parseArgsAndExecute(args)
} catch (e: IncorrectPathException) {
println(e.message)
exit()
}
}
}
/** Custom exception being thrown if user enter incorrect path to file. */
class IncorrectPathException(path: String = "") : Exception("File at $path doesn't exist")
/** Function used for exiting out of program. */
fun exit(): Nothing {
println("bye!")
@ -347,155 +486,11 @@ fun exit(): Nothing {
}
/** Reads commands from the console and storing data about
* the functions they should execute.
*/
class CommandCenter(private val textData: TextData) {
private val exitCommand = Command("exit", ::exit)
private val addCommand = Command("add text") { readNewText(textData) }
private val showStatisticsCommand = Command("show statistics") { getStatistics(textData) }
private val removeTextCommand = Command("remove text") { textData.removeText() }
private val commandsList = listOf(exitCommand, addCommand, showStatisticsCommand, removeTextCommand)
private val commandsNames = commandsList.map { it.name }
private val parser = ArgParser("Text Analyzer", useDefaultHelpShortName = true)
private val add by parser.option(
ArgType.String, "add", "a",
"adds new text to data", "Add warn"
)
private val remove by parser.option(
ArgType.String, "remove", "r",
"removes text from data", "Remove warn"
)
private val stat by parser.option(
ArgType.String, "statistics", "s",
"shows statistics", "Stat warn"
)
private val exit by parser.option(
ArgType.String, "exit", "e",
"exit from program", "exit warn"
)
// fun parseCommand(args: Array<String>) {
// parser.parse(args)
//
// when (add) {
// "" -> readNewText(textData)
// null -> {}
// else ->
// }
// }
/** Stores command and its name */
private class Command(val name: String, val executingFun: () -> Unit)
/** Prints list of names of available commands, requests the name and
* returns corresponding to entered name function.
*/
fun readCommandFromConsole(): () -> Unit {
println("Input one of the commands: ${commandsNames.joinToString()}:")
val commandNameRequest = requestInput(commandsNames)
commandNameRequest.first.exe()
val funName = commandNameRequest.second
return commandsList.find { it.name == funName }!!.executingFun
}
}
/** Function repeating the process of calling functions returned by
* CommandCenter. If ReturnException was thrown, it is caught here,
* last iteration breaks and new one is called.
*/
fun workCycle(commandCenter: CommandCenter) {
while (true) {
try {
(commandCenter.readCommandFromConsole())()
} catch (e: ReturnException) {
println("You have been returned in main menu.")
continue
}
}
}
/** Function reads from console input, check if it isn't available, repeat the request from the console
* in this case and convert input in type T if possible, else throws an exception and repeat the
* request.
*
* @param availableInputs list of available values, the choice among which
* is requested from the console at some stage of the program.
* @param unavailableInputs list of unavailable values.
* @return a pair of function, which could be called after the request from this function to return
* if user want it or continue and value that the user has selected from the list of available.
*/
fun requestInput(
availableInputs: List<Any>? = null,
unavailableInputs: List<Any>? = null
): Pair<InputOutcomeCommand, String?> {
val regexAvailableInputs = availableInputs?.joinToString("|")?.toRegex() ?: Regex(".*")
val regexUnavailableInputs = unavailableInputs?.joinToString("|")?.toRegex() ?: Regex("")
val returnRegex = Regex("return")
while (true) {
val input = readln().trim()
return when {
input.matches(regexAvailableInputs) && !input.matches(regexUnavailableInputs) ->
ContinueCommand() to input
input.matches(returnRegex) -> ReturnCommand() to null
else -> {
println("There isn't this elem in list of available inputs. Try to repeat or enter return to exit in main menu: ")
continue
}
}
}
}
/** Interface used to existing commands objects, whose exe value
* stores function which calls after requesting from console in
* requestInput function.
*/
interface InputOutcomeCommand {
/** Function returned in a Main.requestInput method for returned to
* main menu of application or continuation of the process.
*/
val exe: () -> Unit
}
/** If called exe of this object, program continue executing
* without changes.
*/
class ContinueCommand : InputOutcomeCommand {
override val exe: () -> Unit = {}
}
/** If called exe of this object, a custom ReturnException is thrown
* and process of executing some called command interrupted. Exception
* catches in mainCycle and program command request is repeated.
*/
class ReturnCommand : InputOutcomeCommand {
override val exe: () -> Unit = throw ReturnException()
}
/** Custom exception being thrown if user want to return to the menu. */
class ReturnException : Exception()
fun main() {
fun main(args: Array<String>) {
val textData = TextData()
val commandCenter = CommandCenter(textData)
commandCenter.subcommandsInit()
workCycle(commandCenter)
readAndCallParsing(commandCenter, args)
}