From f397684d4afcdbc89e9e17fea7c6f9de54da05e7 Mon Sep 17 00:00:00 2001 From: mikemirzayanov Date: Wed, 5 Nov 2025 16:50:58 +0300 Subject: [PATCH] FileUtil#readFileCropped --- .../com/codeforces/commons/io/FileUtil.java | 23 ++++++++- .../commons/io/internal/UnsafeFileUtil.java | 51 +++++++++++++++++++ .../codeforces/commons/text/StringUtil.java | 3 +- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/code/src/main/java/com/codeforces/commons/io/FileUtil.java b/code/src/main/java/com/codeforces/commons/io/FileUtil.java index e29c997..dea94cb 100644 --- a/code/src/main/java/com/codeforces/commons/io/FileUtil.java +++ b/code/src/main/java/com/codeforces/commons/io/FileUtil.java @@ -282,6 +282,27 @@ public static String readFile(File file) throws IOException { return Objects.requireNonNull(executeIoOperation(() -> UnsafeFileUtil.readFile(file))); } + /** + * Reads a file, limiting the number of lines read and cropping each line + * if its length exceeds the maximum. + *

+ * This implementation is efficient because it uses Files.lines() for + * lazy, buffered, line-by-line processing, avoiding loading the entire + * file into memory. The .limit() operation short-circuits reading, + * stopping disk access as soon as maxLineCount is reached. + * + * @param file File to be read. + * @param maxLineCount Maximum number of lines to read. + * @param maxLineLength Maximum length of each line to crop to. + * @return String containing file data in cropped form. + * @throws IOException if can't read file (e.g., file doesn't exist, is a directory, or permission issues). + */ + @Nonnull + public static String readFileCropped(File file, int maxLineCount, int maxLineLength) throws IOException { + return Objects.requireNonNull(executeIoOperation(() + -> UnsafeFileUtil.readFileCropped(file, maxLineCount, maxLineLength))); + } + /** * Writes new file into filesystem. Overwrite existing if exists. * Creates parent directory if needed. @@ -1049,7 +1070,7 @@ private static long parseSize(@Nonnull String size, @Nonnegative int lastCharInd /** * Set executable permission for file. * - * @param file File to set permission. + * @param file File to set permission. * @param executable {@code true} to set executable permission. */ public static void setExecutable(@Nonnull File file, boolean executable) { diff --git a/code/src/main/java/com/codeforces/commons/io/internal/UnsafeFileUtil.java b/code/src/main/java/com/codeforces/commons/io/internal/UnsafeFileUtil.java index acabd29..5373f6f 100644 --- a/code/src/main/java/com/codeforces/commons/io/internal/UnsafeFileUtil.java +++ b/code/src/main/java/com/codeforces/commons/io/internal/UnsafeFileUtil.java @@ -6,6 +6,7 @@ import com.codeforces.commons.lang.ObjectUtil; import com.codeforces.commons.math.RandomUtil; import com.codeforces.commons.properties.internal.CommonsPropertiesUtil; +import com.codeforces.commons.text.StringUtil; import com.google.common.primitives.Ints; import com.google.errorprone.annotations.MustBeClosed; import de.schlichtherle.truezip.file.TFile; @@ -32,6 +33,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.codeforces.commons.math.Math.max; @@ -879,6 +882,54 @@ public static byte[] downloadFileAsByteArray( return IoUtil.toByteArray(connection.getInputStream(), maxSize); } + @Nonnull + public static String readFileCropped(File file, int maxLineCount, int maxLineLength) throws IOException { + if (maxLineCount <= 0) { + return ""; + } + + List lines; + boolean moreLinesExist; + + // 1. Read one line more than the limit (maxLineCount + 1) + // try-with-resources ensures the underlying file handle/buffer is closed immediately. + try (Stream linesStream = Files.lines(file.toPath())) { + + // Collect up to maxLineCount + 1 lines. The .limit() operation short-circuits reading. + lines = linesStream + .limit(maxLineCount + 1) + // Apply line-level cropping/shrinking + .map(line -> StringUtil.shrinkTo(line, maxLineLength)) + .collect(Collectors.toList()); + } + + // 2. Check if the (maxLineCount + 1)-th line was present + if (lines.size() > maxLineCount) { + moreLinesExist = true; + // Remove the extra line so it's not included in the output + lines.remove(maxLineCount); + } else { + moreLinesExist = false; + } + + // 3. Assemble the final result string + String lineSeparator = System.lineSeparator(); + + // Join the processed lines + String result = String.join(lineSeparator, lines); + + // Conditionally append the ellipsis line if the file had more content + if (moreLinesExist) { + // Append line separator only if there are existing lines + if (!result.isEmpty()) { + result += lineSeparator; + } + result += "..."; + } + + return result; + } + @SuppressWarnings({"FinalizeDeclaration", "DeserializableClassInSecureContext"}) private static class TemporaryDirectory extends File { private TemporaryDirectory(String pathname) { diff --git a/code/src/main/java/com/codeforces/commons/text/StringUtil.java b/code/src/main/java/com/codeforces/commons/text/StringUtil.java index fbb4934..5a0c181 100644 --- a/code/src/main/java/com/codeforces/commons/text/StringUtil.java +++ b/code/src/main/java/com/codeforces/commons/text/StringUtil.java @@ -1370,7 +1370,8 @@ public static String cropLines(String input, int maxLineLength, int maxLineNumbe return cropLines(input, maxLineLength, maxLineNumber, "...", "..."); } - public static String cropLines(String input, int maxLineLength, int maxLineNumber, String endOfLineReplacement, String endOfContentReplacement) { + public static String cropLines(String input, int maxLineLength, int maxLineNumber, + String endOfLineReplacement, String endOfContentReplacement) { if (isEmpty(input)) { return input; }