Write/Print to the bottom of terminal - java

I'm creating a sort of messaging program, where users can send messages to each other. I would like to make this look like any social messaging app where the most recently received message appears at the bottom of the screen and the previous messages are shown above (older messages appear higher up). Is there a way to print a message (String) to a specific line in terminal, or to the bottom line?
Mac OS x

Oh the days of yore! I remember when every village lad, who dreamed to become a Wizard, knew his ANSI control codes by heart. Surely those byte sequences allowed one in the knowledge to produce awesome visions and bright colors of exotic places on text displays lesser ones thought only as dumb terminals.
But the days are gone, and we have high resolution displays and 3D graphics, mouses and touchscreens. Time to go, you old fool! But wait! In the heart of every terminal window, (we are not speaking about Windows now) there beats a golden heart of old VT100 terminal. So yes, it is possible to control cursor positions and scroll areas with Java console output.
The idea is, that the terminal is not so dump after all. It only waits escape sequences for you to tell what special it should do. Instructions start with escape character, which is non-printable ASCII character 27, usually written in hexadecimal 0x1b or octal 033. After that some human readable stuff follows, most often bracket [, some numerical information and a letter. If more numbers are needed, they are separated by semicolons.
For instance, to change font color to red, you output sequence <ESC>[31m like this:
System.out.println("\033[31mThis should appear red");
You'd better open a separate terminal window for fiddling with the codes, because the display turns easily garbled. In fact the colors of shell prompt and directory listings are implemented by these codes and can easily be personalized.
If you want to separate areas from top and bottom of the window from scrolling, there's a code for that, <ESC>[<top>;<bottom>r where you declare the lines from top and bottom, between where the scrolling happens, for instance: \033[2;22r
Links:
Wikipedia's quite exhaustive article: Ansi escape code
https://en.wikipedia.org/wiki/ANSI_escape_code
Clear list of the codes: http://www.termsys.demon.co.uk/vtansi.htm
And of course answer is not an answer without running code. Right? I have to admit I got little carried away with this. Although it felt strange to use Java for terminal sequences. K&R C or COMMODORE BASIC would have been more fitting. I made very simple demo, but slowed down the printing speed to same level as old 1200 baud modem, so you can easily observe what's happening. Set the SPEED to 0 to disable.
Escape sequences are same kind of markup as HTML text formatting tags, and what's best, The BLINK is still there!
Save as TerminalDemo.java, compile with javac TerminalDemo.java and run with command: java TerminalDemo
import java.io.*;
public class TerminalDemo {
// Default speed in bits per second for serial line simulation
// Set 0 to disable
static final int SPEED = 1200;
// ANSI Terminal codes
static final String ESC = "\033";
static String clearScreen() { return "\033[2J"; }
static String cursorHome() { return "\033[H"; }
static String cursorTo(int row, int column) {
return String.format("\033[%d;%dH", row, column);
}
static String cursorSave() { return "\033[s"; }
static String cursorRestore() { return "\033[u"; }
static String scrollScreen() { return "\033[r"; }
static String scrollSet(int top, int bottom) {
return String.format("\033[%d;%dr", top, bottom);
}
static String scrollUp() { return "\033D"; }
static String scrollDown() { return "\033D"; }
static String setAttribute(int attr) {
return String.format("\033[%dm", attr);
}
static final int ATTR_RESET = 0;
static final int ATTR_BRIGHT = 1;
static final int ATTR_USCORE = 4;
static final int ATTR_BLINK = 5;
static final int ATTR_REVERSE = 7;
static final int ATTR_FCOL_BLACK = 30;
static final int ATTR_FCOL_RED = 31;
static final int ATTR_FCOL_GREEN = 32;
static final int ATTR_FCOL_YELLOW = 33;
static final int ATTR__BCOL_BLACK = 40;
static final int ATTR__BCOL_RED = 41;
static final int ATTR__BCOL_GREEN = 42;
public static void main(String[] args) {
// example string showing some text attributes
String s = "This \033[31mstring\033[32m should \033[33mchange \033[33m color \033[41m and start \033[5m blinking!\033[0m Isn't that neat?\n";
// Reset scrolling, clear screen and bring cursor home
System.out.print(clearScreen());
System.out.print(scrollScreen());
// Print example string s
slowPrint(s);
// some text attributes
slowPrint("This "
+ setAttribute(ATTR_USCORE) + "should be undescored\n"
+ setAttribute(ATTR_RESET)
+ setAttribute(ATTR_FCOL_RED) + "and this red\n"
+ setAttribute(ATTR_RESET)
+ "some "
+ setAttribute(ATTR_BRIGHT)
+ setAttribute(ATTR_FCOL_YELLOW)
+ setAttribute(ATTR_BLINK) + "BRIGHT YELLOW BLINKIN\n"
+ setAttribute(ATTR_RESET)
+ "could be fun.\n\n"
+ "Please press ENTER");
// Wait for ENTER
try { System.in.read(); } catch(IOException e) {e.printStackTrace();}
// Set scroll area
slowPrint(""
+ clearScreen()
+ scrollSet(2,20)
+ cursorTo(1,1)
+ "Cleared screen and set scroll rows Top: 2 and Bottom: 20\n"
+ cursorTo(21,1)
+ "Bottom area starts here"
+ cursorTo(2,1)
+ "");
// print some random text
slowPrint(randomText(60));
// reset text attributes, reset scroll area and set cursor
// below scroll area
System.out.print(setAttribute(ATTR_RESET));
System.out.print(scrollScreen());
System.out.println(cursorTo(22,1));
}
// Slow things down to resemble old serial terminals
private static void slowPrint(String s) {
slowPrint(s, SPEED);
}
private static void slowPrint(String s, int bps) {
for (int i = 0; i < s.length(); i++) {
System.out.print(s.charAt(i));
if(bps == 0) continue;
try { Thread.sleep((int)(8000.0 / bps)); }
catch(InterruptedException ex)
{ Thread.currentThread().interrupt(); }
}
}
// Retursn a character representation of sin graph
private static String randomText(int lines) {
String r = "";
for(int i=0; i<lines; i++) {
int sin = (int)Math.abs((Math.sin(1.0/20 * i)*30));
r += setAttribute((sin / 4) + 30);
for(int j=0; j<80; j++) {
if(j > 40 + sin)
break;
r += (j < (40-sin)) ? " " : "X";
}
r += setAttribute(ATTR_RESET) + "\n";
}
return r;
}
}

Related

Java code issue [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I'm having an issue with my code. It won't compile and every time I try to fix it I get an error. I can't see what the problem with it is. I may be just overlooking or looking too hard. I haven't done Java coding since 2006 until I took this class. The class doesn't go a lot into the coding and I'm trying to work off of my memory. I have been working on this code and looking at this code since I wrote it over a week ago. Can someone look and tell me what I screwed up. It's already past-due. I already showed the teacher but haven't got any help and my school doesn't currently have a tutor for this class during this semester. Please help! Code and errors are below:
Errors:
/*
* File: Guitar2.java
* Author: Eppards,Michael
* Date: June 11, 2018
*/
class Guitar2 {
private int numStrings;
private double guitarLength;
private Color guitarColor;
private Manufacturer guitarManufacturer;
enum Manufacturer {
GIBSON,
FENDER,
IBANEZ;
}
enum Color {
RED,
BLUE,
BLACK;
}
// Default Constructor
private Guitar2() {
this.numStrings = 6;
this.guitarLength = 28.2;
this.guitarColor = Color.RED;
this.guitarManufacturer = Manufacturer.GIBSON;
}
// Argument Constructor
private Guitar2(int numStrings, double guitarLength, Color guitarColor, Manufacturer guitarMaker) {
this.numStrings = numStrings;
this.guitarLength = guitarLength;
this.guitarColor = guitarColor;
this.guitarManufacturer = guitarMaker;
}
// Getters
public int getNumStrings() {
return numStrings;
}
public double getGuitarLength() {
return guitarLength;
}
public Color getGuitarColor() {
return guitarColor;
}
public Manufacturer getGuitarManufacturer() {
return guitarManufacturer;
}
// playGuitar method for 16 randomly selected musical notes
enum Notes {
A(1),
B(4),
C(1),
D(2),
E(0.25),
F(0.5),
G(0.5);
double duration;
private Notes(double duration) {
this.duration = duration;
}
}
public String playGuitar() {
String play = "[";
int a;
int b;
// initiate a random number
Random rn = new Random();
// for loop to select random notes
for (int k = 0; k < 16; k++) {
a = rn.nextInt(7);
b = rn.nextInt(5);
play = play + Notes.values()[a] + "(" + Notes.values()[a].duration + ")";
if (k != 15) play = play + ",";
}
play = play + ']';
return play;
}
// used to return the info in a string
public String toString() {
String infoString = "(numStrings=" + this.numStrings + ", guitarLength=" + this.guitarLength + ",guitarManufacturer=" + this.guitarManufacturer + ",guitarColor=" + (this.guitarColor == null ? "none" : this.guitarColor.name()) + ")";
return infoString;
}
}
Guitar2.java:80: error: cannot find symbol
Random rn = new Random();
^
symbol: class Random
location: class Guitar2
Guitar2.java:80: error: cannot find symbol
Random rn = new Random();
^
symbol: class Random
location: class Guitar2
2 errors
-Hello, World!-
There were quite a few issues with the code that caused it to not compile. I fixed all of them (I think) and have written out what was wrong:
Public needs to be changed to public
The second Guitar constructor is missing all it's parameters.
Enums need to be referenced by their name, you can't just type in their values.
Your curly braces were all over the place. There were way more closing than opening, so I deleted a bunch of them at the end and ones that were tossed into the middle of the class.
Methods must be within classes.
Enums can't be declared inside methods, only inside classes.
Variable declarations must have a space between the type and the name of the variable. Example: int_a; needs to be int a;
Your Duration enum has no constructor that accepts and int.
Getting a random value of an enum based on its index is done by doing Notes.values()[a];, not Notes[a];
No idea where the return Optional.empty(); line should be, so I deleted it.
You're missing the import for the Random class.
Strings can't be on multiple lines.
.orElse() is not a method. Deleted this, unsure what it was supposed to do.
colorName() is not a method. I changed it to: (this.guitarColor == null ? "none" : this.guitarColor.name()).
The name of the variable in the toString() method is not the same as what you try to return. I changed them both to infoString.
You have a TON of typos, namely with things like Manufacturer being Manufacur and return being rturn.
When initializing a variable, you must have a space after new.
Your getter methods do not return the correct type.
The name of the variable numString is repeatedly referred to as numStrings. I added the s to the original variable declaration.
Below are a list of things you really SHOULD change, but you don't need to change.
All enum names should be in ALL CAPS.
Method names should be written using camelCase. Example: myVal would have a getter named getMyVal().
Have blank lines between methods, constructors, and types.
Have a space after the start of a comment. Example: // Comment Bad Example: //Comment
Have some sort of sorting pattern for enums. For notes, it should be alphabetical.
There is no need to have another enum for the duration of notes. This can easily be done in the note itself.
There is no need to do String.valueOf(someObject) for concatenation. It is better to do someObject.toString() and best to just leave it as someObject because the runtime will do that operation for you.
There should be a space before and after arithmetic operators like + and -.
I would highly recommend you read up on some basic Java tutorials and look at the Google Java Style Guide. It is what I use and what I'm sure a lot of people use to determine how code should look.
With all the changes and with the proper use of style, your code looks like this:
import java.util.Random;
/*
* File: Guitar.java
* Author: Eppards,Michael
* Date: June 11, 2018
*/
class Guitar {
private int numStrings;
private double guitarLength;
private Color guitarColor;
private Manufacturer guitarManufacturer;
enum Manufacturer {
GIBSON,
FENDER,
IBANEZ;
}
enum Color {
RED,
BLUE,
BLACK;
}
enum Notes {
A(1),
B(4),
C(1),
D(2),
E(0.25),
F(0.5),
G(0.5);
double duration;
private Notes(double duration) {
this.duration = duration;
}
}
// Default Constructor
private Guitar() {
this.numStrings = 6;
this.guitarLength = 28.2;
this.guitarColor = Color.RED;
this.guitarManufacturer = Manufacturer.GIBSON;
}
// Argument Constructor
private Guitar(int numStrings, double guitarLength, Color guitarColor, Manufacturer guitarMaker) {
this.numStrings = numStrings;
this.guitarLength = guitarLength;
this.guitarColor = guitarColor;
this.guitarManufacturer = guitarMaker;
}
// Getters
public int getNumStrings() {
return numStrings;
}
public double getGuitarLength() {
return guitarLength;
}
public Color getGuitarColor() {
return guitarColor;
}
public Manufacturer getGuitarManufacturer() {
return guitarManufacturer;
}
// playGuitar method for 16 randomly selected musical notes
public String playGuitar() {
String play = "[";
int a;
int b;
// initiate a random number
Random rn = new Random();
// for loop to select random notes
for (int k = 0; k < 16; k++) {
a = rn.nextInt(7);
b = rn.nextInt(5);
play = play + Notes.values()[a] + "(" + Notes.values()[a].duration + ")";
if (k != 15) play = play + ",";
}
play = play + ']';
return play;
}
// used to return the info in a string
public String toString() {
String infoString = "(numStrings=" + this.numStrings + ", guitarLength=" + this.guitarLength + ",guitarManufacturer=" + this.guitarManufacturer + ",guitarColor=" + (this.guitarColor == null ? "none" : this.guitarColor.name()) + ")";
return infoString;
}
}
Hope this helps! Friendly reminder that I made a LOT of changes to this code. You really should go look at the steps I took and make the changes yourself, otherwise you could get in trouble with your school for submitting work that is not your own.

How can I display a short length version of a file name in java inside of a jlist

I have a JList that displays a filelist. The style I have it set to looks good with a FileFilter set to only show files and directories with names that are 15 characters long, however I still want to show the files that are longer than that, just show the first 15 characters or so. Basically, I want it to show this:
If I have a text file that says "1234567891234567.txt" - that has 20 characters including the ".txt" and it won't show up in the list. But I want it to show something like this:
"12345...567.txt" or something similar. Is there a way to do this?
Would I have to create a seperate array and copy everything over, and edit the value of the new array to be no longer than 15 characters? I tried looking for a function that would change the name of the file but I couldn't find any. Suggestions?
You can check the length of file name and abbreviate it if it contains more than 20 characters, like the method below:
private static String getShortName(String fileName){
if(fileName.length() <= 20){
return fileName;
}
String extension = fileName.substring(fileName.lastIndexOf("."));
String name = fileName.substring(0, fileName.lastIndexOf("."));
return name.substring(0, 5) + "..." + name.substring(name.length() - 4) + extension;
}
public static void main(String[] args) throws Exception {
System.out.println(getShortName("123.txt"));
System.out.println(getShortName("123rewe.txt"));
System.out.println(getShortName("123fdsfdsfdasfadsfdsgafgaf.txt"));
}
Please note that it won't work if the extension itself is more than 20 characters or file name does not have any extension. However, you can modify it as per your requirement.
Coming up with an abbreviated string that will fit in a certain amount of space isn’t as trivial as it may sound. Sure, you could just make sure your text is no longer than 15 characters, but all Swing look-and-feels assign a variable-width font to JLists. The system look-and-feel will use whatever font the underlying desktop uses for lists, which also is a variable-width font in all desktops I’m aware of.
Which means, the 20-character string IIIIIIIIIIIIIIII.txt and the 20-character string WWWWWWWWWWWWWWWW.txt are not the same width. Truncating each of them to fit in the JList’s space will not be as simple as making them 15 characters long.
Fortunately, you can use a FontMetrics to calculate a string’s visual size.
The simplest, though hardly most efficient, algorithm is to whittle down a string one character at a time until it fits in the JList’s width:
static <T> JList<T> createList(Collection<T> items) {
JList<T> list = new JList<T>(new Vector<T>(items)) {
private static final long serialVersionUID = 1;
#Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
};
list.setCellRenderer(new DefaultListCellRenderer() {
private static final long serialVersionUID = 1;
private Insets insets = new Insets(0, 0, 0, 0);
#Override
public Component getListCellRendererComponent(JList<?> list,
Object value,
int index,
boolean selected,
boolean focused) {
insets = list.getInsets(insets);
int listWidth =
list.getWidth() - insets.left - insets.right - 4;
if (listWidth > 0 &&
value != null &&
!(value instanceof Icon)) {
FontMetrics metrics = list.getFontMetrics(list.getFont());
Graphics g = list.getGraphics();
String text = value.toString();
while (text.length() > 1 &&
metrics.getStringBounds(text, g).getWidth() > listWidth) {
int midpoint = text.length() / 2;
if (text.charAt(midpoint) != '\u2026') {
// Replace center character with ellipsis.
text = text.substring(0, midpoint) + '\u2026'
+ text.substring(midpoint + 1);
} else {
// Remove character before or after ellipsis.
if (text.length() % 2 == 0) {
midpoint--;
} else {
midpoint++;
}
text = text.substring(0, midpoint)
+ text.substring(midpoint + 1);
}
}
value = text;
g.dispose();
}
return super.getListCellRendererComponent(list, value, index,
selected, focused);
}
});
return list;
}
(Notice that “…” is not three period characters, but rather the ellipsis character. What’s the difference? The ellipsis is kerned differently, justified differently, read by screen readers differently, can’t be broken up by word-wrap, and is simply the correct punctuation. You wouldn’t use two apostrophes to represent a double-quote.)
I naïvely start by replacing the center character in each string, regardless of the width of the characters on either side of that character, but a more intelligent and possibly more visually pleasing approach would be to use the character visually located at the center, using TextLayout.hitTestChar.

What unicode characters are best for drawing a squarewave in a string

I am trying to represent a square wave pulse train in a string, from an array of stored GPIO events and timings. the code is working but I need better Unicode characters for the transitions.
this is my current method
public String waveform()
{
String s = "";
int pulses;
int pulseWidth;
pulseWidth = events.get(1).duration; //mostly right!
for (RF433Event e: events)
{
pulses = e.duration/pulseWidth;
if (e.gpioEvent.getEdge() == PinEdge.RISING)
{
// rising edge so for the duration it was low
for (int i = 0; i<pulses; i++) s = s+'_';
s = s+"\u02E9";
} else
{
// falling edge so for the duration it was high
for (int i = 0; i<pulses; i++) s = s+"\u0305";
s = s+"\u02E5";
}
}
return s;
}
The output looks like this in the Intellij console window
but strangely is not appearing on the RPi, do I need to install something else on the Pi?
After much experimentation this works for me
public String waveform()
{
String s = "";
int pulses;
int pulseWidth;
// Characters tried for drawing the pulse train
// Low line - "_", "\u0332" Combining Low Line, "\uFF3F"; FULLWIDTH LOW LINE
// High line - "\u0305" COMBINING OVERLINE, "\u203E" over line
// Vertical - "\u20D2" COMBINING LONG VERTICAL LINE OVERLAY, "\u007C" Vertical line, "\u02E9" MODIFIER LETTER EXTRA-LOW TONE BAR
if (events.get(0).duration > 50000) {return "Excessive duration in pluse 0 "+events.get(0).duration;}
pulseWidth = 100; //gives a reasonable pulse train
for (RF433Event e: events)
{
pulses = e.duration/pulseWidth;
if (e.gpioEvent.getEdge() == PinEdge.RISING)
{
// rising edge so for the duration it was low
for (int i = 0; i<pulses; i++) s = s+ "_";
s = s+"\u20D2";
} else
{
// falling edge so for the duration it was high
for (int i = 0; i<pulses; i++) s = s+"\u0305";
s = s+"\u20D2";
}
}
return s;
}
these are the results
I like the idea of using "combining characters",
but find them harder to edit.
Here are other unicode options I've used or tried.
box drawings (light or heavy), which (as noted) need 2 rows:
1 ┌────┐ ┏━━━━┓
0 ──┘ └── ━━┛ ┗━━
"lower one eighth block" (2581) with "lower three quarters block" (2586) or "full block" (2588). (Depending on font and line spacing, "full block" can reach the previous line.)
▁▆▆▆▁▁▇▇▁▁
▁▆▆▆▁▁▇▇▁▁
"solidus" (slash), "reverses solidus" (backslash), "horizontal scan line 1" (23BA), and "horizontal scan line 9" (23BD) - for non-vertical transitions.
⎺⎺⎺⎺⎺\⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽/⎺⎺⎺⎺⎺\⎽⎽

Class to count variables design issue

I'm new to OO programing and having a bit of trouble with the design of my program to use the concepts. I have done the tutorials but am still having problem.
I have a recursion that takes a value of items(could be anything in this example, stocks) and figures out what number of them are needed to equal a specific value(in this code 100). This part works but I want to know if a stock's weighting exceeds a threshold. Originally I approached this problem with a method that did a for loop and calculated the entire list of values but this is super inefficient because its doing it on every loop of the recursion. I thought this would be a good time to try to learn classes because I could use a class to maintain state information and just increment the value on each loop and it'll let me know when the threshold is hit.
I think I have the code but I don't fully understand how to design this problem with classes. So far it runs the loop each step of the recursion because I'm initially the class there. Is there a better way to design this? My end goal is to be notified when a weighting is exceeded(which I can somewhat already do) but I want to do in way that uses the least bit of resources(avoiding inefficient/unnecessary for loops)
Code(Here's the entire code I have been using to learn but the problem is with the Counter class and its location within the findVariables method):
import java.util.Arrays;
public class LearningClassCounting {
public static int[] stock_price = new int[]{ 20,5,20};
public static int target = 100;
public static void main(String[] args) {
// takes items from the first list
findVariables(stock_price, 100, new int[] {0,0,0}, 0, 0);
}
public static void findVariables(int[] constants, int sum,
int[] variables, int n, int result) {
Counter Checker = new Counter(stock_price, variables);
if (n == constants.length) {
if (result == sum) {
System.out.println(Arrays.toString(variables));
}
} else if (result <= sum){ //keep going
for (int i = 0; i <= 100; i++) {
variables[n] = i;
Checker.check_total_percent(n, i);
findVariables(constants, sum, variables, n+1, result+constants[n]*i);
}
}
}
}
class Counter {
private int[] stock_price;
private int[] variables;
private int value_so_far;
public Counter(int[] stock_price, int[] variables) {
this.stock_price = stock_price;
this.variables = variables;
for (int location = 0; location < variables.length; location++) {
//System.out.println(variables[location] + " * " + stock_price[location] + " = " + (variables[location] * stock_price[location]) );
value_so_far = value_so_far + (variables[location] * stock_price[location]);
}
//System.out.println("Total value so far is " + value_so_far);
//System.out.println("************");
}
public void check_total_percent(int current_location, int percent) {
// Check to see if weight exceeds threshold
//System.out.println("we are at " + current_location + " and " + percent + " and " + Arrays.toString(variables));
//System.out.println("value is " + stock_price[current_location] * percent);
//formula I think I need to use is:
if (percent == 0) {
return;
}
int current_value = (stock_price[current_location] * percent);
int overall_percent = current_value/(value_so_far + current_value);
if (overall_percent > 50 ) {
System.out.println("item " + current_location + " is over 50%" );
}
}
}
What you're describing sounds like a variant of the famous knapsack problem. There are many approaches to these problems, which are inherently difficult to calculate.
Inherently, one may need to check "all the combinations". The so-called optimization comes from backtracking when a certain selection subset is already too large (e.g., if 10 given stocks are over my sum, no need to explore other combinations). In addition, one can cache certain subsets (e.g., if I know that X Y and Z amount to some value V, I can reuse that value). You'll see a lot of discussion of how to approach these sort of problems and how to design solutions.
That being said, my view is that while algorithmic problems of this sort may be important for learning how to program and structure code and data structures, they're generally a very poor choice for learning object-oriented design and modelling.

Ideal method to truncate a string with ellipsis

I'm sure all of us have seen ellipsis' on Facebook statuses (or elsewhere), and clicked "Show more" and there are only another 2 characters or so. I'd guess this is because of lazy programming, because surely there is an ideal method.
Mine counts slim characters [iIl1] as "half characters", but this doesn't get around ellipsis' looking silly when they hide barely any characters.
Is there an ideal method? Here is mine:
/**
* Return a string with a maximum length of <code>length</code> characters.
* If there are more than <code>length</code> characters, then string ends with an ellipsis ("...").
*
* #param text
* #param length
* #return
*/
public static String ellipsis(final String text, int length)
{
// The letters [iIl1] are slim enough to only count as half a character.
length += Math.ceil(text.replaceAll("[^iIl]", "").length() / 2.0d);
if (text.length() > length)
{
return text.substring(0, length - 3) + "...";
}
return text;
}
Language doesn't really matter, but tagged as Java because that's what I'm mostly interested in seeing.
I like the idea of letting "thin" characters count as half a character. Simple and a good approximation.
The main issue with most ellipsizings however, are (imho) that they chop of words in the middle. Here is a solution taking word-boundaries into account (but does not dive into pixel-math and the Swing-API).
private final static String NON_THIN = "[^iIl1\\.,']";
private static int textWidth(String str) {
return (int) (str.length() - str.replaceAll(NON_THIN, "").length() / 2);
}
public static String ellipsize(String text, int max) {
if (textWidth(text) <= max)
return text;
// Start by chopping off at the word before max
// This is an over-approximation due to thin-characters...
int end = text.lastIndexOf(' ', max - 3);
// Just one long word. Chop it off.
if (end == -1)
return text.substring(0, max-3) + "...";
// Step forward as long as textWidth allows.
int newEnd = end;
do {
end = newEnd;
newEnd = text.indexOf(' ', end + 1);
// No more spaces.
if (newEnd == -1)
newEnd = text.length();
} while (textWidth(text.substring(0, newEnd) + "...") < max);
return text.substring(0, end) + "...";
}
A test of the algorithm looks like this:
I'm shocked no one mentioned Commons Lang StringUtils#abbreviate().
Update: yes it doesn't take the slim characters into account but I don't agree with that considering everyone has different screens and fonts setup and a large portion of the people that land here on this page are probably looking for a maintained library like the above.
It seems like you might get more accurate geometry from the Java graphics context's FontMetrics.
Addendum: In approaching this problem, it may help to distinguish between the model and view. The model is a String, a finite sequence of UTF-16 code points, while the view is a series of glyphs, rendered in some font on some device.
In the particular case of Java, one can use SwingUtilities.layoutCompoundLabel() to effect the translation. The example below intercepts the layout call in BasicLabelUI to demonstrate the effect. It may be possible to use the utility method in other contexts, but the appropriate FontMetrics would have to be be determined empirically.
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicLabelUI;
/** #see http://stackoverflow.com/questions/3597550 */
public class LayoutTest extends JPanel {
private static final String text =
"A damsel with a dulcimer in a vision once I saw.";
private final JLabel sizeLabel = new JLabel();
private final JLabel textLabel = new JLabel(text);
private final MyLabelUI myUI = new MyLabelUI();
public LayoutTest() {
super(new GridLayout(0, 1));
this.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(Color.blue), new EmptyBorder(5, 5, 5, 5)));
textLabel.setUI(myUI);
textLabel.setFont(new Font("Serif", Font.ITALIC, 24));
this.add(sizeLabel);
this.add(textLabel);
this.addComponentListener(new ComponentAdapter() {
#Override
public void componentResized(ComponentEvent e) {
sizeLabel.setText(
"Before: " + myUI.before + " after: " + myUI.after);
}
});
}
private static class MyLabelUI extends BasicLabelUI {
int before, after;
#Override
protected String layoutCL(
JLabel label, FontMetrics fontMetrics, String text, Icon icon,
Rectangle viewR, Rectangle iconR, Rectangle textR) {
before = text.length();
String s = super.layoutCL(
label, fontMetrics, text, icon, viewR, iconR, textR);
after = s.length();
System.out.println(s);
return s;
}
}
private void display() {
JFrame f = new JFrame("LayoutTest");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(this);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new LayoutTest().display();
}
});
}
}
If you're talking about a web site - ie outputting HTML/JS/CSS, you can throw away all these solutions because there is a pure CSS solution.
text-overflow:ellipsis;
It's not quite as simple as just adding that style to your CSS, because it interracts with other CSS; eg it requires that the element has overflow:hidden; and if you want your text on a single line, white-space:nowrap; is good too.
I have a stylesheet that looks like this:
.myelement {
word-wrap:normal;
white-space:nowrap;
overflow:hidden;
-o-text-overflow:ellipsis;
text-overflow:ellipsis;
width: 120px;
}
You can even have a "read more" button that simply runs a javascript function to change the styles, and bingo, the box will re-size and the full text will be visible. (in my case though, I tend to use the html title attribute for the full text, unless it's likely to get very long)
Hope that helps. It's a much simpler solution that trying to mess calculate the text size and truncate it, and all that. (of course, if you're writing a non-web-based app, you may still need to do that)
There is one down-side to this solution: Firefox doesn't support the ellipsis style. Annoying, but I don't think critical -- It does still truncate the text correctly, as that is dealt with by by overflow:hidden, it just doesn't display the ellipsis. It does work in all the other browsers (including IE, all the way back to IE5.5!), so it's a bit annoying that Firefox doesn't do it yet. Hopefully a new version of Firefox will solve this issue soon.
[EDIT]
People are still voting on this answer, so I should edit it to note that Firefox does now support the ellipsis style. The feature was added in Firefox 7. If you're using an earlier version (FF3.6 and FF4 still have some users) then you're out of luck, but most FF users are now okay. There's a lot more detail about this here: text-overflow:ellipsis in Firefox 4? (and FF5)
For me this would be ideal -
public static String ellipsis(final String text, int length)
{
return text.substring(0, length - 3) + "...";
}
I would not worry about the size of every character unless I really know where and in what font it is going to be displayed. Many fonts are fixed width fonts where every character has same dimension.
Even if its a variable width font, and if you count 'i', 'l' to take half the width, then why not count 'w' 'm' to take double the width? A mix of such characters in a string will generally average out the effect of their size, and I would prefer ignoring such details. Choosing the value of 'length' wisely would matter the most.
Using Guava's com.google.common.base.Ascii.truncate(CharSequence, int, String) method:
Ascii.truncate("foobar", 7, "..."); // returns "foobar"
Ascii.truncate("foobar", 5, "..."); // returns "fo..."
How about this (to get a string of 50 chars):
text.replaceAll("(?<=^.{47}).*$", "...");
public static String getTruncated(String str, int maxSize){
int limit = maxSize - 3;
return (str.length() > maxSize) ? str.substring(0, limit) + "..." : str;
}
If you're worried about the ellipsis only hiding a very small number of characters, why not just check for that condition?
public static String ellipsis(final String text, int length)
{
// The letters [iIl1] are slim enough to only count as half a character.
length += Math.ceil(text.replaceAll("[^iIl]", "").length() / 2.0d);
if (text.length() > length + 20)
{
return text.substring(0, length - 3) + "...";
}
return text;
}
I'd go with something similar to the standard model that you have. I wouldn't bother with the character widths thing - as #Gopi said it is probably goign to all balance out in the end. What I'd do that is new is have another paramter called something like "minNumberOfhiddenCharacters" (maybe a bit less verbose). Then when doign the ellipsis check I'd do something like:
if (text.length() > length+minNumberOfhiddenCharacters)
{
return text.substring(0, length - 3) + "...";
}
What this will mean is that if your text length is 35, your "length" is 30 and your min number of characters to hide is 10 then you would get your string in full. If your min number of character to hide was 3 then you would get the ellipsis instead of those three characters.
The main thing to be aware of is that I've subverted the meaning of "length" so that it is no longer a maximum length. The length of the outputted string can now be anything from 30 characters (when the text length is >40) to 40 characters (when the text length is 40 characters long). Effectively our max length becomes length+minNumberOfhiddenCharacters. The string could of course be shorter than 30 characters when the original string is less than 30 but this is a boring case that we should ignore.
If you want length to be a hard and fast maximum then you'd want something more like:
if (text.length() > length)
{
if (text.length() - length < minNumberOfhiddenCharacters-3)
{
return text.substring(0, text.length() - minNumberOfhiddenCharacters) + "...";
}
else
{
return text.substring(0, length - 3) + "...";
}
}
So in this example if text.length() is 37, length is 30 and minNumberOfhiddenCharacters = 10 then we'll go into the second part of the inner if and get 27 characters + ... to make 30. This is actually the same as if we'd gone into the first part of the loop (which is a sign we have our boundary conditions right). If the text length was 36 we'd get 26 characters + the ellipsis giving us 29 characters with 10 hidden.
I was debating whether rearranging some of the comparison logic would make it more intuitive but in the end decided to leave it as it is. You might find that text.length() - minNumberOfhiddenCharacters < length-3 makes it more obvious what you are doing though.
In my eyes, you can't get good results without pixel math.
Thus, Java is probably the wrong end to fix this problem when you are in a web application context (like facebook).
I'd go for javascript. Since Javascript is not my primary field of interest, I can't really judge if this is a good solution, but it might give you a pointer.
Most of this solutions don't take font metrics into account, here is a very simple but working solution for java swing that i have used for years now.
private String ellipsisText(String text, FontMetrics metrics, Graphics2D g2, int targetWidth) {
String shortText = text;
int activeIndex = text.length() - 1;
Rectangle2D textBounds = metrics.getStringBounds(shortText, g2);
while (textBounds.getWidth() > targetWidth) {
shortText = text.substring(0, activeIndex--);
textBounds = metrics.getStringBounds(shortText + "...", g2);
}
return activeIndex != text.length() - 1 ? shortText + "..." : text;
}
For simple cases, I have used String.format for this.
Here I abbreviate to max 10 chars and add ellipses:
String abbreviate(String longString) {
return String.format("%.10s...", longString);
}
Little known fact the "precision" numbers in the format pattern is used for truncation in strings.
Add your own length-check, of course, if you want to make ellipses conditional. (I was shortening a JWT for logging, so I know it's going to be longer)
As a bonus, if the String is already shorter than the precision, there is no padding, it simply leaves it as is.
> System.out.println(abbreviate("This is a very long string"));
> System.out.println(abbreviate("Shorty"));
This is a ...
Shorty...
You can also simply implement like this:
mb_strimwidth($string, 0, 120, '...')
Thanks.

Categories