细微之处见真章之字符串超长省略功能

2021-08-31 15:00:36 浏览数 (1)

一、背景

有这样一个需求:如果一个字符串超过某个长度,则超过该长度的部分用省略号代替。

很多人会觉得这 so easy,有点 Java基础的同学都可以简单编写出来。

那么我们来分析这个简单的问题。

二、编码

2.1 思路

思路很简单,判断size 是否小于字符串长度,如果小于,则超过部分替换为 ... 即可。

2.2 编码

我们编码要多考虑一些:

  1. 为了健壮性,我们要进行参数校验;
  2. 另外如果想写一个完善的工具类,可以支持自定义省略符;

我们来编写工具类:

代码语言:javascript复制
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;

public class StringUtil {

    /**
     * 超过 maxSize 的部分用省略号代替
     *
     * @param originStr 原始字符串
     * @param maxSize   最大长度
     */
    public static String abbreviate(String originStr, int maxSize) {

        return abbreviate(originStr, maxSize, null);
    }

    /**
     * 超过 maxSize 的部分用省略号代替
     *
     * @param originStr    原始字符串
     * @param maxSize      最大长度
     * @param abbrevMarker 省略符
     */
    public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {

        Preconditions.checkArgument(maxSize > 0, "size 必须大于0");

        if (StringUtils.isEmpty(originStr)) {
            return StringUtils.EMPTY;
        }

        String defaultAbbrevMarker = "...";

        if (originStr.length() < maxSize) {
            return originStr;
        }

        return originStr.substring(0, maxSize)   StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
    }
}

这里借助了 commons-lang3 包里的StringUtils,和guava 包的Preconditions,如果项目里没引入这些包,可以自己手动实现也很简单。

写完了怎么验证正确性呢?

作为一个合格的程序,肯定要写单元测试的嘛!

代码语言:javascript复制
public class StringUtilTest {

   @Test
    public void abbreviateLess() {
        String input = "123456789";
        String abbreviate = StringUtil.abbreviate(input, 11);
        Assert.assertEquals(input, abbreviate);
    } 

    @Test
    public void abbreviateCommon() {
        String input = "123456789";
        String abbreviate = StringUtil.abbreviate(input, 3);
        Assert.assertEquals("123...", abbreviate);
    }

    @Test
    public void abbreviateWithMarker() {
        String input = "123456789";
        String abbreviate = StringUtil.abbreviate(input, 3, "***");
        Assert.assertEquals("123***", abbreviate);
    }

    @Test(expected = IllegalArgumentException.class)
    public void abbreviateWithNegativeSize() {
        String input = "123456789";
        String abbreviate = StringUtil.abbreviate(input, -3);
    }

}

发现功能通过。

2.3 思考问题?

如果就这么完了,是不是也没太大价值呢?

2.3.1 如果是emoji 表情,占两个字符,如果截取到了第一个字符,会不会有问题?

写单测验证一下,果然有问题。

作为优秀的程序员,我们是不是应该和产品交流一下这种情况该怎么办呢?

假设产品说:这种情况就把整个表情不要了。

我们对此工具函数做出修改:

代码语言:javascript复制
/**
     * 超过 maxSize 的部分用省略号代替
     *
     * @param originStr    原始字符串
     * @param maxSize      最大长度
     * @param abbrevMarker 省略符
     */
    public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {

        Preconditions.checkArgument(maxSize > 0, "size 必须大于0");

        if (StringUtils.isEmpty(originStr)) {
            return StringUtils.EMPTY;
        }

        String defaultAbbrevMarker = "...";

        if (originStr.length() < maxSize) {
            return originStr;
        }

        // 截取前maxSize 个字符
        String head = originStr.substring(0, maxSize);

        // 最后一个字符是高代理项,则移除掉
        char lastChar = head.charAt(head.length() - 1);
        if (Character.isHighSurrogate(lastChar)) {
            head = head.substring(0, head.length() - 1);
        }


        return head   StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
    }

重新运行单元测试,发现得到了我们想要的效果。

关于  unicode 详细内容参考维基百科,相关字符的用法参考下面的文章:

https://www.ibm.com/developerworks/cn/java/j-unicode/

2.3.2 如何可以写的更完善?

上面的做法看似很完美了,但是如何写的更完善呢? what? 这还不行吗?!

我们看下 commons-lang3 的 StringUtils 工具类的源码:

代码语言:javascript复制
    /**
     * Returns either the passed in CharSequence, or if the CharSequence is
     * empty or {@code null}, the value of {@code defaultStr}.
     *
     *      * StringUtils.defaultIfEmpty(null, "NULL")  = "NULL"
     * StringUtils.defaultIfEmpty("", "NULL")    = "NULL"
     * StringUtils.defaultIfEmpty(" ", "NULL")   = " "
     * StringUtils.defaultIfEmpty("bat", "NULL") = "bat"
     * StringUtils.defaultIfEmpty("", null)      = null
     * 
     * @param  the specific kind of CharSequence
     * @param str  the CharSequence to check, may be null
     * @param defaultStr  the default CharSequence to return
     *  if the input is empty ("") or {@code null}, may be null
     * @return the passed in CharSequence, or the default
     * @see StringUtils#defaultString(String, String)
     */
    public static  T defaultIfEmpty(final T str, final T defaultStr) {
        return isEmpty(str) ? defaultStr : str;
    }

可以发现,源码给出了常见参数的返回值,用起来特别容易。

因此我们做出下面的修改:

代码语言:javascript复制
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;

public class StringUtil {

    /**
     * 超过 maxSize 的部分用省略号代替
     * 
     * 使用范例:
     * 1 不超过取所有
     * StringUtil.abbreviate("123456789", 11) = "123456789"
     * 
     * 2 超过最大长度截取并补充省略号
     * StringUtil.abbreviate("123456789", 3) = "123..."
     * 
     * 3 emoji表情被截断则丢弃前面的字符(整个表情)
     * StringUtil.abbreviate("123456789??", 10) = "123456789..."
     *
     * @param originStr 原始字符串
     * @param maxSize   最大长度
     */
    public static String abbreviate(String originStr, int maxSize) {

        return abbreviate(originStr, maxSize, null);
    }

    /**
     * 超过 maxSize 的部分用省略号代替
     * 
     * 使用范例:
     * 
     * StringUtil.abbreviate("123456789"", 3, "***") = "123..."
     *
     * @param originStr    原始字符串
     * @param maxSize      最大长度
     * @param abbrevMarker 省略符
     */
    public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {

        Preconditions.checkArgument(maxSize > 0, "size 必须大于0");

        if (StringUtils.isEmpty(originStr)) {
            return StringUtils.EMPTY;
        }

        String defaultAbbrevMarker = "...";

        if (originStr.length() < maxSize) {
            return originStr;
        }

        // 截取前maxSize 个字符
        String head = originStr.substring(0, maxSize);

        // 最后一个字符是高代理项,则移除掉
        char lastChar = head.charAt(head.length() - 1);
        if (Character.isHighSurrogate(lastChar)) {
            head = head.substring(0, head.length() - 1);
        }


        return head   StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
    }
}

最为优秀的程序员,我们编写工具类时,可以把工具类的常见输入和输出在注释中给出,方便使用者。

三、总结

这个简单的功能,实现很容易,写好却没那么容易。

可以加上参数校验,加上单元测试,加上注释,加上emoji表情问题处理等。

很多新手总是觉得很多问题很简单,但是简单的功能代码能否写的严谨,是一件值得思考的问题。

另外希望大家能从各方面吸收源码的精华,而不是想当然地读源码,源码的注释,源码的设计模式,源码的编写思路都是非常有价值的东西。

编程在细微之处见真章,希望大家在平时编程时能够养成好的习惯,努力做一个有追求的优秀的程序员。

0 人点赞