input输入框按字节统计长度以及按字符长度截取内容

需求说明

有个需求如下,一个input框需要自定义可输入的字符长度范围以及按字节统计已输入的长度和按字符截取最大长度的内容,
例如,
需求1:在一个字符占3个字节的情况下(此处为举例,具体字节字符的关系会在下面详细说明),限制输入框最大只能输入30个长度的字符,那么在输入内容"1"时,输入框提示应该是1/30,在输入内容"12"时,提示应该是1/30,在输入内容"123"时,提示也是1/30,在输入内容"1234"时,提示内容应该是2/30,在输入内容"你1"时,提示内容为2/30,基本情况下可以认为中文字符等价于汉字
需求2:现有字符串"有1个需求如下,现一个input框需要自定义可输入的长度范围以及按字节",以上字符串按照截取长度30个字符来算,需要截取90长度的字节,那么最后一个汉字"节"只能截取到前两个字节,最后一个字节是第91个字节,那么导致的问题就是最后一个汉字会变成乱码

前置知识

首先了解字符和字节之间的关系:
英文字母和中文汉字(包括中文标点)在不同字符集编码下所占的字节数
英文字母:
字节数 : 1;编码:GB2312
字节数 : 1;编码:GBK
字节数 : 1;编码:GB18030
字节数 : 1;编码:ISO-8859-1
字节数 : 1;编码:UTF-8
字节数 : 4;编码:UTF-16
字节数 : 2;编码:UTF-16BE
字节数 : 2;编码:UTF-16LE
中文汉字:
字节数 : 2;编码:GB2312
字节数 : 2;编码:GBK
字节数 : 2;编码:GB18030
字节数 : 1;编码:ISO-8859-1
字节数 : 3;编码:UTF-8
字节数 : 4;编码:UTF-16
字节数 : 2;编码:UTF-16BE
字节数 : 2;编码:UTF-16LE,
由以上可知一个字符占多少字节是和字符集相关的,解决这个问题,那么我们可以在computed里面计算一个变量,得到当前环境下一个字符占多少个字节,代码如下:

computed:{
    //一个中文字符占多少字节
    charToByte: function () {
      // let str2 = '中';
      let str2 = ';';
      let byte2 = this.stringToByte(str2)
      return byte2.length;
    },
  },
  methods: {
    //字符串转字节数组
    stringToByte(str) {
      const bytes = new Array()
      let len, c
      len = str.length
      for (let i = 0; i < len; i++) {
        c = str.charCodeAt(i)
        if (c >= 0x010000 && c <= 0x10FFFF) {
          bytes.push(((c >> 18) & 0x07) | 0xF0)
          bytes.push(((c >> 12) & 0x3F) | 0x80)
          bytes.push(((c >> 6) & 0x3F) | 0x80)
          bytes.push((c & 0x3F) | 0x80)
        } else if (c >= 0x000800 && c <= 0x00FFFF) {
          bytes.push(((c >> 12) & 0x0F) | 0xE0)
          bytes.push(((c >> 6) & 0x3F) | 0x80)
          bytes.push((c & 0x3F) | 0x80)
        } else if (c >= 0x000080 && c <= 0x0007FF) {
          bytes.push(((c >> 6) & 0x1F) | 0xC0)
          bytes.push((c & 0x3F) | 0x80)
        } else {
          bytes.push(c & 0xFF)
        }
      }
      return bytes
    }
  }

有了这个变量,下面就可以实现需求

首先是需求一:

<el-input v-model="inputVal" @input="handleInput"/>

先给input绑定@input事件,在用户输入的时候,能实时统计已经输入的字符长度:

 methods: {
    handleInput(val) {
      let byte = this.stringToByte(val);
      this.charNum = Math.ceil(byte.length / this.charToByte);
    }
  }

这里需要向上取整,因为输入1个英文字符时,也要提示长度已经输入了1

需求二:
此需求实现思路如下,首先把需要截取的字符串转换为字节数组,然后根据限制的字符长度截取对应长度的字节数组,再将字节数组转换为字符串
难点一:需要标记哪些字节数组是对应中文字符,哪些是对应英文字符,如"你1"转换为字节数组为[228, 189, 160, 49],前三个字节是汉字"你"所占的字节,最后一位是数字"1"所占字节,解决此问题代码如下:

//获取字节数组每一位代表的是否为中文
getHanZiByteArr(str){
	 let byte = this.stringToByte(str);
	 const resultArr = new Array;
	 for (let i = 0; i < byte.length; i++) {
	   const obj = {
	     ishanzi: false,//是否是汉字字节
	     groupFlag: null//同组标记,一个汉字转换来的3个字节的此属性值相同
	   }
	   resultArr.push(obj);
	 }
	 let strArr = str.split("");
	 //匹配中文标点字符的正则
	 const bdReg = /[\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]/
	 //匹配中文汉字字符的正则
	 const hzReg =/[\u4E00-\u9FA5]/;
	 let realIndex = 0;
	 for (let i = 0; i < strArr.length; i++) {
	   let singleStr = strArr[i];
	   //生成唯一的标记UUID
	   let groupId = this.generateUUID();
	   if(bdReg.test(singleStr) || hzReg.test(singleStr)){
	     //是中文字符
	     for (let k = 0;k<this.charToByte;k++){
	       resultArr[realIndex].ishanzi = true;
	       resultArr[realIndex].groupFlag = groupId;
	       realIndex++;
	     }
	   }else{
	     resultArr[realIndex].ishanzi = false;
	     resultArr[realIndex].groupFlag = groupId;
	     realIndex++;
	   }
	 }
	 return resultArr;
}

难点二,需要根据截取的最后一位字节的ishanzi属性值判断是汉字字节还是非汉字字节,并做相应处理
情况一:如果最后一位字节为false,表示截取到限制长度的最后一位刚好是非中文字节,因为非汉字字节本身只占一个字节,那么不存在被截断的情况,即如下情况"你好123哈哈"截取3个字符,截取刚好是"你好123",此情况直接将截取好的字节数组转换为字符串即可
情况二:如果最后一位字节为true,表示截取到限制长度的最后一位是中文字符,那么判断和此字节的groupFlag相同的字节个数是否等于前面计算属性中一个中文字符对应的字节个数,如果相等,即如下情况"你好123哈哈"截取4个字符刚好是"你好123哈",此种情况同情况一将截取好的字节数组转换为字符串即可
情况三:情况二中如果和此字节的groupFlag相同的字节个数不等于(一定是小于)前面计算属性中一个中文字符对应的字节个数,表示一个汉字所占的三个字节被截断了,即如下情况,"你1好123哈哈"截取4个字符长度,那么对应的字节数组[228, 189, 160, 49, 229, 165, 189, 49, 50, 51, 229, 147, 136, 229, 147, 136]被截取成[228, 189, 160, 49, 229, 165, 189, 49, 50, 51, 229, 147],第一个汉字"哈"被截掉了最后一个字节136,那么转换为字符串后就会乱码,此种情况应该舍弃不完整的字节数组,再转换为字符串。
完整版代码如下:

<template>
  <div>
    请输入内容:<el-input v-model="inputVal" @input="handleInput"/>
    所占字符长度:<span>{{ charNum }}</span>
    <el-divider></el-divider>
    目标内容:<el-input v-model="targetStr" />
    截取长度:<el-input v-model="limit" />
    <el-button type="primary" @click="handleSubstr">截取</el-button>
    <el-divider></el-divider>
    截取结果:<span>{{afterSubstr}}</span>
  </div>
</template>

<script>
export default {
  name: 'subs',
  data() {
    return {
      charNum: 0,
      inputVal: '',
      limit:0,
      afterSubstr:'',
      targetStr:''
    }
  },
  computed:{
    //一个中文字符占多少字节
    charToByte: function () {
      // let str = '中';
      let str2 = ';';
      // let byte = this.stringToByte(str)
      let byte2 = this.stringToByte(str2)
      return byte2.length;
    },
  },
  methods: {
    handleSubstr(){
      this.afterSubstr = this.subAndTransToStr(this.targetStr,this.limit);
    },
    //截取前n个字符
    subAndTransToStr(str,n){
      let resourceArr = this.stringToByte(str);
      let finalByteArr = []
      let hanZiByteArr = this.getHanZiByteArr(str);
      const sumNum = n * this.charToByte
      if(sumNum >= hanZiByteArr.length ){
        finalByteArr = resourceArr;
      }else{
        finalByteArr = hanZiByteArr.slice(0,sumNum);
        if(finalByteArr[finalByteArr.length - 1].ishanzi){
          let count = 0;
          //最后一位为true,需要判断同groupFlag的charToByte个元素的ishanzi是否全为true,是代表最后一位刚好是汉字,否则就是截断了,直接舍弃
          var lastGroupFlag = finalByteArr[finalByteArr.length - 1].groupFlag;
          finalByteArr.forEach((item,index)=>{
            if(item.groupFlag == lastGroupFlag){
              count++;
            }
          });
          if(count == this.charToByte){
            finalByteArr = resourceArr.slice(0,finalByteArr.length);
          }else{
            finalByteArr = resourceArr.slice(0,finalByteArr.length - count);
          }
        }else{
          finalByteArr = resourceArr.slice(0,finalByteArr.length);
        }
      }
      let result = this.utf8ByteToUnicodeStr(finalByteArr);
      return result;
    },
    //获取字节数组每一位代表的是否为中文
    getHanZiByteArr(str){
      let byte = this.stringToByte(str);

      const resultArr = new Array;
      for (let i = 0; i < byte.length; i++) {
        const obj = {
          ishanzi: false,
          groupFlag: null
        }
        resultArr.push(obj);
      }

      let strArr = str.split("");
      const bdReg = /[\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]/
      const hzReg =/[\u4E00-\u9FA5]/;
      let realIndex = 0;
      for (let i = 0; i < strArr.length; i++) {
        let singleStr = strArr[i];
        let groupId = this.generateUUID();
        if(bdReg.test(singleStr) || hzReg.test(singleStr)){
          //是中文字符
          for (let k = 0;k<this.charToByte;k++){
            resultArr[realIndex].ishanzi = true;
            resultArr[realIndex].groupFlag = groupId;
            realIndex++;
          }
        }else{
          resultArr[realIndex].ishanzi = false;
          resultArr[realIndex].groupFlag = groupId;
          realIndex++;
        }
      }
      return resultArr;
    },
    handleInput(val) {
      let byte = this.stringToByte(val);
      this.charNum = Math.ceil(byte.length / this.charToByte);
    },
    //字符串转字节数组
    stringToByte(str) {
      const bytes = new Array()
      let len, c
      len = str.length
      for (let i = 0; i < len; i++) {
        c = str.charCodeAt(i)
        if (c >= 0x010000 && c <= 0x10FFFF) {
          bytes.push(((c >> 18) & 0x07) | 0xF0)
          bytes.push(((c >> 12) & 0x3F) | 0x80)
          bytes.push(((c >> 6) & 0x3F) | 0x80)
          bytes.push((c & 0x3F) | 0x80)
        } else if (c >= 0x000800 && c <= 0x00FFFF) {
          bytes.push(((c >> 12) & 0x0F) | 0xE0)
          bytes.push(((c >> 6) & 0x3F) | 0x80)
          bytes.push((c & 0x3F) | 0x80)
        } else if (c >= 0x000080 && c <= 0x0007FF) {
          bytes.push(((c >> 6) & 0x1F) | 0xC0)
          bytes.push((c & 0x3F) | 0x80)
        } else {
          bytes.push(c & 0xFF)
        }
      }
      return bytes

    },
    //字节数组转字符串(处理了乱码)
    utf8ByteToUnicodeStr(utf8Bytes) {
      let unicodeStr = ''
      for (let pos = 0; pos < utf8Bytes.length;) {
        const flag = utf8Bytes[pos]
        let unicode = 0
        if ((flag >>> 7) === 0) {
          unicodeStr += String.fromCharCode(utf8Bytes[pos])
          pos += 1

        } else if ((flag & 0xFC) === 0xFC) {
          unicode = (utf8Bytes[pos] & 0x3) << 30
          unicode |= (utf8Bytes[pos + 1] & 0x3F) << 24
          unicode |= (utf8Bytes[pos + 2] & 0x3F) << 18
          unicode |= (utf8Bytes[pos + 3] & 0x3F) << 12
          unicode |= (utf8Bytes[pos + 4] & 0x3F) << 6
          unicode |= (utf8Bytes[pos + 5] & 0x3F)
          unicodeStr += String.fromCharCode(unicode)
          pos += 6

        } else if ((flag & 0xF8) === 0xF8) {
          unicode = (utf8Bytes[pos] & 0x7) << 24
          unicode |= (utf8Bytes[pos + 1] & 0x3F) << 18
          unicode |= (utf8Bytes[pos + 2] & 0x3F) << 12
          unicode |= (utf8Bytes[pos + 3] & 0x3F) << 6
          unicode |= (utf8Bytes[pos + 4] & 0x3F)
          unicodeStr += String.fromCharCode(unicode)
          pos += 5

        } else if ((flag & 0xF0) === 0xF0) {
          unicode = (utf8Bytes[pos] & 0xF) << 18
          unicode |= (utf8Bytes[pos + 1] & 0x3F) << 12
          unicode |= (utf8Bytes[pos + 2] & 0x3F) << 6
          unicode |= (utf8Bytes[pos + 3] & 0x3F)
          unicodeStr += String.fromCharCode(unicode)
          pos += 4

        } else if ((flag & 0xE0) === 0xE0) {
          unicode = (utf8Bytes[pos] & 0x1F) << 12

          unicode |= (utf8Bytes[pos + 1] & 0x3F) << 6
          unicode |= (utf8Bytes[pos + 2] & 0x3F)
          unicodeStr += String.fromCharCode(unicode)
          pos += 3

        } else if ((flag & 0xC0) === 0xC0) { //110
          unicode = (utf8Bytes[pos] & 0x3F) << 6
          unicode |= (utf8Bytes[pos + 1] & 0x3F)
          unicodeStr += String.fromCharCode(unicode)
          pos += 2

        } else {
          unicodeStr += String.fromCharCode(utf8Bytes[pos])
          pos += 1
        }
      }
      return unicodeStr
    },
    generateUUID() {
      var d = new Date().getTime();
      if (window.performance && typeof window.performance.now === "function") {
        d += performance.now(); //use high-precision timer if available
      }
      var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
      });
      return uuid;
    }
  }
}
</script>
<style scoped>

</style>
闲来无事记录一下,如有谬误欢迎指正!
上一篇:【LeetCode笔记】剑指Offer 43. 1~n 整数中1出现的次数(Java、数位dp、偏数学)


下一篇:40、最短路(Dijkstra)