天天看點

一種基于正則的狀态變化事件觸發機制

問題描述

​ 在部落客開發一個項目時,遇到了這麼一個需求,一共有護士、病人、藥品、裝置、病床等标簽(rfid),如果在采集器範圍内采集到的标簽有狀态變化(rfid标簽離開/進入采集到的範圍),就會觸發事件,如果是多個标簽的狀态同時變化,會觸發特殊的場景。

問題分析

​ 在收到這個需求後,部落客思考了下,如果使用簡單的if-else來處理的話,我腦袋裡已經有很多蒼蠅在嗡嗡嗡的飛了,首先來說,如何去判斷狀态是否變化就會讓人煩不勝煩,何況還有這麼多種類的标簽,此時使用List或是Set結構都不是很合适,再者遇到更複雜的場景,比如需要判斷護士、藥品、病人同時采集到30s才可以判定為用藥場景,光是這一個場景,估計代碼就要好幾百行了。最後整個邏輯的代碼量會非常的大,且if-else過多導緻難以了解和維護,如果在換個開發,那簡直就是災難了。

​ 部落客在經過一下午的思考整理後,部落客想到一個解決方案,也就是我們題目上說的,使用正則,因為正則對字元串處理上的強大,我們将狀态的變化轉換為字元串,之後通過寫相應的正則來判斷是否符合場景,把複雜的場景的狀态通過正則來簡單的處理。

算法描述

​ 首先,我們約定的資料格式如下:data:N-100002,P-20001,M-30001,E-40001,B-50001…每個标簽用英文逗号分隔,單個标簽用-來分隔标簽的類型,類型定義如下:

在@Getter
@AllArgsConstructor
public enum RfidTypeEnum {

	N("N", "護士RFID标簽類型"),

	P("P", "病人RFID标簽類型"),

	E("E", "裝置RFID标簽類型"),

	M("M", "藥品RFID标簽類型"),

	B("B", "病床RFID标簽類型"),
	;

	/**
	 * 标簽RFID的類型
	 */
	private final String type;

	/**
	 * 标簽類型的描述
	 */
	private final String description;

  /**
	 * 根據String轉換成對應的枚舉值
	 */
	public static RfidTypeEnum getFrom(String type) {
		for (RfidTypeEnum tmp : RfidTypeEnum.values()) {
			if (type.equals(tmp.getType())) {
				return tmp;
			}
		}
		return null;
	}
}
           

我們收到目前時刻采集到的标簽資料,如果包含護士标簽,那麼我們就将護士的狀态置為0,如果不包含,則表示護士不在,狀态置為1,其他的标簽我們也是同樣的規則,之後我們按照NPMEB順序(即護士、病人、藥品、裝置、病床)将這五種标簽的目前狀态組合,假設接收的資料為N-100002,P-20001,那我們轉換的目前狀态為00111轉換代碼如下所示:

//此處代碼為前面調用的代碼
{
  //...
  String[] rfids = data.split(",");
	List<String> rfidList = new ArrayList<>(Arrays.asList(rfids));
	//目前狀态
	String nowStatus = castData(rfidList);
  //...
}
/**
	 * 根據标簽清單産生采集到的标簽
	 * 根據順序N P M E B
	 * 0表示在,1表示不在
	 * 因為狀态變化出發操作與采集到的資料中有多少個無關,是以此時不關心數量
	 * 如果rfidList中隻有一個NULL,狀态全部為11111(如果采集不到資料,會接收一個NULL字元串,用于做時間間隔)
	 *
	 * @param rfidList
	 * @return
	 */
	private String castData(List<String> rfidList) {
		StringBuilder result = new StringBuilder("11111");
		//相容采集不到資料的問題 字元串為"NULL"
		if (rfidList.size() == 1 && "NULL".equals(rfidList.get(0))) {
			return result.toString();
		}
		for (String rfid : rfidList) {
			String type = rfid.split("-")[0];
			result.replace(index.indexOf(type), index.indexOf(type) + 1, "0");
		}
		return result.toString();
	}
	
	
           

之後我們将目前的狀态和上一時刻的狀态進行比對,假設上一時刻沒有采集到任何的标簽,那上一時刻的狀态字元串為11111,此時,我們将狀态的變化使用一個具體的數字來進行表示,比如1->1,實際意義為連續兩次都沒有采集到的,我們用1來進行表示,具體的狀态變化的表示我們定義如下:

@Getter
@AllArgsConstructor
public enum StatusChangeEnum {
	/**
	 * 1->1
	 * 一直不在
	 */
	A("11", 1),
	/**
	 * 1->0
	 * 不在變為在
	 */
	B("10", 2),
	/**
	 * 0->1
	 * 在變為不在
	 */
	C("01", 3),
	/**
	 * 0->0
	 * 一直在
	 */
	D("00", 4),
	;

	private String des;

	private Integer value;

	/**
	 * 由String 轉換為枚舉值
	 * @param des
	 * @return
	 */
	public static StatusChangeEnum getFrom(String des){
		for(StatusChangeEnum tmp: StatusChangeEnum.values()){
			if(des.equals(tmp.getDes())){
				return tmp;
			}
		}
		return null;
	}
}
           

具體的轉換代碼如下,其中historyStatus是一個全部變量,資料結構為一個List,用來存儲之前的曆史狀态,排在最後的資料為上一時刻的狀态,我們将目前的狀态的字元串(00111)和上一時刻(11111)的狀态字元串按照下述代碼轉換,得到的狀态變換的statusChange的值為:22111。

//此處代碼為前面調用的代碼
{
  //...
  String statusChange = castAndOperation(nowStatus);
  //...
}
/**
	 * 将目前狀态和上一狀态做比對,得到狀态變化字元串
	 *
	 * @param nowStatus
	 * @return
	 */
	private String castAndOperation(String nowStatus) {
		//log.info("history data: 【{}】", historyStatus);
		String beforeStatus = historyStatus.get(historyStatus.size() - 1);
		StringBuilder result = new StringBuilder("");
		for (int i = 0; i < beforeStatus.length(); i++) {
			String change = "" + beforeStatus.charAt(i) + nowStatus.charAt(i);
			result.append(StatusChangeEnum.getFrom(change).getValue());
		}
		return result.toString();
	}
           

到這裡,我們就得到狀态變化的字元串statusChange了,我們就可以使用正規表達式來判斷标簽資料的變換了,比如我們要判斷護士的狀态是否變化,我們就可以使用正則來進行判斷了,因為狀态變換有兩種情況:0->1或1->0,是以我們隻需護士所在的位置(NPMEB)數字為2或3即可。

private static final Pattern nurseChange = Pattern.compile("^[2|3]\\d{4}");
           

如果想要更複雜的場景,比如我們在上面講的用藥場景,需要護士、病人、藥品都在,這種狀态就有兩種情況:1->0或0->0,即要目前的狀态都需要為0,因為我們的正規表達式如下:

private static final Pattern medication = Pattern.compile("1{3}\d{2}");

------------------------------------------分割線----------

擷取rfid資料

​ 上面我們将了狀态變化的部分,如果狀态變化後,去觸發事件時還需要标簽中的rfid值,就需要我們維護另一個和historyStatus一樣的資料了,我們定義為historyRfid,用來存放每次收到的資料,資料結構也是List,我們将一次接收的rfid資料按照以下格式來處理,相同類型的标簽資料使用-分隔,不同類型的标簽使用英文,分隔,處理代碼如下:

/**
	 * 根據标簽清單得到rfid清單
	 * rfid清單按照N P M E B排列
	 * 多個rfid按照xxx,xxx組合  英文,為分隔符
	 * 如果某個不存在,使用NULL标記
	 * 不同的類型的用-分割
	 * 資料示例
	 *
	 * @param rfidList
	 * @return
	 */
	private String getRfifData(List<String> rfidList) {
		//去除重複的rfid
		Set<String> rfidSet = new HashSet<>();
		StringBuilder[] rfids = {new StringBuilder(), new StringBuilder("-"),
				new StringBuilder("-"), new StringBuilder("-"), new StringBuilder("-")};
		for (String rfid : rfidList) {
			String[] nums = rfid.split("-");
			//防止空指針異常
			if (index.contains(nums[0]) && !rfidSet.contains(nums[1])) {
				//将rfid拼接到對應的位置
				rfids[index.indexOf(nums[0])].append(nums[1]).append(",");
				//将rfid加入到集合中,用作下次判重
				rfidSet.add(nums[1]);
			}
		}
		StringBuilder result = new StringBuilder();
		for (StringBuilder tem : rfids) {
			if (tem.length() <= 1) {
				//如果此次資料不包含此類标簽,使用NULL标記
				tem.append("NULL");
			} else {
				//替換掉多餘的 ,
				tem.replace(tem.lastIndexOf(","), tem.lastIndexOf(",") + 1, "");
			}
			result.append(tem);
		}
		return result.toString();
	}
           

如果我們想要擷取rfid資料,假設隻擷取狀态變化的rfid資料,因為有兩種情況:0->1或1->0,這也就需要我們從目前的接收到的資料或者上一時刻接收的資料或者兩者擷取rfid資料。擷取代碼如下,其中方法中的typeSet參數為所需的資料類型的集合:

/**
	 * 将上面需要擷取狀态變化的資料抽取出來
	 * 可以根據所需資料的集合擷取狀态未變化的rfid資料清單
	 *
	 * @param rfidList
	 * @param typeSet
	 * @return
	 */
	private List<String> getStatusUnchangeRfidData(List<String> rfidList, Set<String> typeSet) {
		List<String> rfidDataList = new ArrayList<>();
		for (String rfid : rfidList) {
			String[] nums = rfid.split("-");
			if (typeSet.contains(nums[0])) {
				rfidDataList.add(nums[1]);
			} else {
				log.info("無用資料:【{}】,過濾", nums);
			}
		}
		return rfidDataList;
	}

/**
	 * 将上面需要擷取狀态變化的資料抽取出來
	 * 可以根據目前序列和上一序列,擷取狀态變化的rfid資料清單
	 *
	 * @param rfidList
	 * @param statusChange
	 * @param typeSet
	 * @return
	 */
	private List<String> getStatusChangeRfidData(List<String> rfidList, String statusChange, Set<String> typeSet) {
		List<String> rfidDataList = new ArrayList<>();
		//擷取目前的和上一時刻的rfid清單
		String beforeRfid = historyRfid.get(historyRfid.size() - 1);
		String nowRfid = getRfifData(rfidList);
		List<String> beforeRfids = Arrays.asList(beforeRfid.split("-"));
		List<String> nowRfids = Arrays.asList(nowRfid.split("-"));
		for (int i = 0; i < statusChange.length(); i++) {
			int status = Integer.parseInt("" + statusChange.charAt(i));
			//設定資料  隻有狀态發生變化才發送0->1||1->0
			String[] rfidArray;
			if (status == 2 || status == 3) {
				//1->0 可從目前擷取  0->1 從上一時刻擷取
				rfidArray = status == 2 ? nowRfids.get(i).split(",") : beforeRfids.get(i).split(",");
				Integer dataStatus = status == 2 ? 0 : 1;
				//資料異常,跳過這條資料
				if ("NULL".equals(rfidArray[0])) {
					continue;
				}
				for (String rfid : rfidArray) {
					if (typeSet.contains("" + index.charAt(i))) {
						rfidDataList.add(rfid);
					}
				}
			}
		}
		return rfidDataList;
	}
           

詳細代碼

​ 下面我們給出方法的具體調用,因為調用的函數上面都已提供,是以這裡隻是接口的主體代碼,具體如下所示:

//import ...

@Slf4j
public class dataTrigerTest {


	private static final String index = "NPMEB";
	private static final Integer cacheCount = 60;
	//場景組裝資料使用
	private static final Set<String> commonSet = new HashSet<>();
	private static final Set<String> pharmacySet = new HashSet<>();
	private static final Set<String> equipmentSet = new HashSet<>();
	private static final Set<String> medicationSet = new HashSet<>();
	private static final Set<String> pmoveSet = new HashSet<>();
	private static final Set<String> medicalWasteSet = new HashSet<>();
	private static List<String> historyStatus = new ArrayList<>();
	private static List<String> historyRfid = new ArrayList<>();

  //初始化資料
	static {
		historyStatus.add("11111");
		//初始時,沒有rfid資料,置為NULL
		historyRfid.add("NULL");
		pharmacySet.add("N");
		pharmacySet.add("M");
		medicationSet.addAll(pharmacySet);
		medicationSet.add("P");
		equipmentSet.add("E");
		equipmentSet.add("B");
		commonSet.addAll(medicationSet);
		commonSet.addAll(equipmentSet);
		pmoveSet.add("P");
		pmoveSet.add("B");
		medicalWasteSet.addAll(pharmacySet);
	}

	private static final Pattern medication = Pattern.compile("^[2|4]{3}");
	private static final Pattern nurseChange = Pattern.compile("^[2|3]\\d{4}");

	public String processDataChange(String data) {
		//process data
		//data:N-100002,B-20001,...
		log.info("接收的資料為:【{}】", data);
		if (StringUtils.isEmpty(data)) {
			log.error("接收資訊為空");
			return null;
		}
		String[] rfids = data.split(",");
		List<String> rfidList = new ArrayList<>(Arrays.asList(rfids));
		//目前狀态變化
		String nowStatus = castData(rfidList);
		//目前rfid   --主要為了擷取狀态0->1時,上一個狀态的rfid
		String nowRfids = getRfifData(rfidList);
		log.info("轉換後的資料為:【{}】【{}】", nowStatus, nowRfids);
		String statusChange = castAndOperation(nowStatus);
		log.info("狀态變化為:【{}】", statusChange);
		if (medication.matcher(statusChange).find()) {
			log.info("用藥場景");
		} else if(nurseChange.matcher(statusChange).find()){
			log.info("護士狀态變化");
		} else{
			log.info("未觸發場景");
		}

		historyStatus.add(nowStatus);
		historyRfid.add(nowRfids);
		//清除資料-->記憶體中儲存過多資料會導緻APP閃退
		clearCacheRfidData();
		return "OK";
	}

	private String castData(List<String> rfidList) {
		//...
	}

	/**
	 * 詳細代碼參考上面代碼
	 */
	private String getRfifData(List<String> rfidList) {
		//...
	}

	/**
	 * 将目前狀态和上一狀态做比對,得到狀态變化字元串
	 *
	 * @param nowStatus
	 * @return
	 */
	private String castAndOperation(String nowStatus) {
		//...
	}


	/**
	 * 将上面需要擷取狀态變化的資料抽取出來
	 * 可以根據所需資料的集合擷取String中有的rfid
	 * 資料形如:1001,1002-NULL-3001,3002-NULL-NULL
	 *
	 * @param rfidString
	 * @param typeSet
	 * @return
	 */
	private List<String> getRfidDataFromString(String rfidString, Set<String> typeSet) {
		List<String> rfidDataList = new ArrayList<>();
		List<String> rfidList = Arrays.asList(rfidString.split("-"));
		for (String type : typeSet) {
			int typeIndex = index.indexOf(type);
			List<String> rfids = Arrays.asList(rfidList.get(typeIndex).split(","));
			for (String rfid : rfids) {
				rfidDataList.add(rfid);
			}

		}
		return rfidDataList;
	}

	private List<String> getStatusUnchangeRfidData(List<String> rfidList, Set<String> typeSet) {
		//...
	}

	
	private List<String> getStatusChangeRfidData(List<String> rfidList, String statusChange, Set<String> typeSet) {
		//...
	}

	/**
	 * 清除存儲的緩存資料,保留設定的記錄數量
	 */
	private void clearCacheRfidData() {
		if (historyStatus.size() > 200 && historyStatus.size() == historyRfid.size()) {
			while (historyStatus.size() > cacheCount) {
				historyStatus.remove(0);
				historyRfid.remove(0);
			}
			log.info("清除後的狀态資料:【{},{}】", historyStatus.size(), historyStatus);
			log.info("清除後的rfid資料:【{},{}】", historyRfid.size(), historyRfid);
		}
		//兩個資料不一緻  需要調整
		if (historyStatus.size() > 200 && historyStatus.size() != historyRfid.size()) {
			int remainCount = cacheCount;
			for (int i = 1; i <= cacheCount; i++) {
				String status = historyStatus.get(historyStatus.size() - i);
				String rfid = historyRfid.get(historyRfid.size() - i);
				if (!judgeStatusAndRfid(status, rfid)) {
					//此時資料異常,清空前序的所有資料
					remainCount = i - 1;
					break;
				}
			}
			//隻保留正确的資料或者最新的60條無誤的資料
			while (historyStatus.size() > remainCount) {
				historyStatus.remove(0);
			}
			while (historyRfid.size() > remainCount) {
				historyRfid.remove(0);
			}
		}
	}

	private boolean judgeStatusAndRfid(String status, String rfid) {
		List<String> rfidList = Arrays.asList(rfid.split("-"));
		for (int j = 0; j < 5; j++) {
			//狀态為0--存在,需要判斷對應的rfid是否存在
			//狀态為1--不存在,需要判斷對應的rfid是否為NULL
			if ((status.charAt(j) - '0' == 0 && "NULL".equals(rfidList.get(j)))
					|| (status.charAt(j) - '0' == 1 && !"NULL".equals(rfidList.get(j)))) {
				return false;
			}
		}
		return true;
	}

	@Test
	public void testPost(){
		processDataChange("N-10001");
		processDataChange("NULL");
		processDataChange("N-10001,P-20001,M-30001");
	}
}
           

最後clearCacheRfidData()函數提供清除緩存的服務,因為如果緩存過多的曆史資料,會占用虛拟機的過多記憶體導緻記憶體溢出,是以我們會将資料進行清除,clearCacheRfidData還提供一個資料糾對的功能,如果historyStatus、historyRfid兩個緩存隊列中的資料數量不一緻,還會将資料進行校對,隻保留對的資料,清除異常的資料。

總結

走過路過,如果有用,留下一個贊呗^_^----------。