Browse Source

Map: Compare precision against 4 not 0 [#792]

gnosygnu 4 years ago
parent
commit
316c6c6a58

+ 14 - 10
400_xowa/src/gplx/xowa/xtns/mapSources/Map_dd2dms_func_tst.java

@@ -1,6 +1,6 @@
 /*
 XOWA: the XOWA Offline Wiki Application
-Copyright (C) 2012-2017 gnosygnu@gmail.com
+Copyright (C) 2012-2020 gnosygnu@gmail.com
 
 XOWA is licensed under the terms of the General Public License (GPL) Version 3,
 or alternatively under the terms of the Apache License Version 2.0.
@@ -13,12 +13,16 @@ The terms of each license can be found in the source code repository:
 GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
 Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
-package gplx.xowa.xtns.mapSources; import gplx.*; import gplx.xowa.*; import gplx.xowa.xtns.*;
-import org.junit.*;
-public class Map_dd2dms_func_tst {
-	@Before public void init()				{fxt.Reset();} private final Xop_fxt fxt = new Xop_fxt();
-	@Test  public void Example()			{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|precision=4}}"					, "{{test}}"	, "14° 34' 48"");}
-	@Test  public void Plus()				{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|precision=4|plus=pos}}"			, "{{test}}"	, "14° 34' 48" pos");}
-	@Test  public void Ws()					{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58| precision = 4 | plus = pos }}"	, "{{test}}"	, "14° 34' 48" pos");}
-	@Test  public void Nested_pfunc()		{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|{{#if:2|precision=2}}}}"		, "{{test|3}}"	, "14° 34'");}	// handle "{{#if:2|precision=2}}" -> "precision=2"
-}
+package gplx.xowa.xtns.mapSources;
+
+import gplx.xowa.Xop_fxt;
+import org.junit.Before;
+import org.junit.Test;
+
+public class Map_dd2dms_func_tst {
+	@Before public void init()				{fxt.Reset();} private final Xop_fxt fxt = new Xop_fxt();
+	@Test  public void Example()			{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|precision=4}}"					, "{{test}}"	, "14° 34' 48"");}
+	@Test  public void Plus()				{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|precision=4|plus=pos}}"			, "{{test}}"	, "14° 34' 48" pos");}
+	@Test  public void Ws()					{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58| precision = 4 | plus = pos }}"	, "{{test}}"	, "14° 34' 48" pos");}
+	@Test  public void Nested_pfunc()		{fxt.Test_parse_tmpl_str_test("{{#dd2dms: 14.58|{{#if:2|precision=2}}}}"		, "{{test|3}}"	, "14° 35'");}	// handle "{{#if:2|precision=2}}" -> "precision=2"
+}

+ 304 - 291
400_xowa/src/gplx/xowa/xtns/mapSources/Map_math.java

@@ -1,6 +1,6 @@
 /*
 XOWA: the XOWA Offline Wiki Application
-Copyright (C) 2012-2017 gnosygnu@gmail.com
+Copyright (C) 2012-2020 gnosygnu@gmail.com
 
 XOWA is licensed under the terms of the General Public License (GPL) Version 3,
 or alternatively under the terms of the Apache License Version 2.0.
@@ -13,293 +13,306 @@ The terms of each license can be found in the source code repository:
 GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
 Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
-package gplx.xowa.xtns.mapSources; import gplx.*; import gplx.xowa.*; import gplx.xowa.xtns.*;
-import gplx.core.primitives.*; import gplx.core.btries.*;
-import gplx.xowa.parsers.*; import gplx.xowa.parsers.tmpls.*;
-class Map_math {// REF.MW:MapSources_math.php
-	private int word_idx_nsew;
-	private double[] rv = new double[4];
-	private byte dir_id;
-	private int prec;
-	private int step;
-	public double Dec() {return dec;} private double dec;
-	public int Error() {return error;} private int error;
-	public double Coord_dec() {return coord_dec;} private double coord_dec;
-	public double Coord_deg() {return coord_deg;} private double coord_deg;
-	public double Coord_min() {return coord_min;} private double coord_min;
-	public double Coord_sec() {return coord_sec;} private double coord_sec;
-	public byte[] Coord_dir_ns() {return coord_dir_ns;} private byte[] coord_dir_ns;
-	public byte[] Coord_dir_ew() {return coord_dir_ew;} private byte[] coord_dir_ew;
-	public boolean Ctor(byte[] input, int prec, byte[] dir, int until_step) {
-		try {
-			New_coord(input, dir, prec);
-			if (until_step > 1 && error == 0)
-				Set_coord();
-		} catch (Exception e) {
-			Err_.Noop(e);
-			error = -128;
-		}
-		return error == 0;
-	}
-	public void Fail(Xop_ctx ctx, byte[] src, Xot_invk self, Bry_bfr bfr, byte[] pfunc_name) {
-		String page_str = ctx.Page().Url().To_str();
-		String pfunc_name_str = String_.new_u8(pfunc_name);
-		String self_str = String_.new_u8(src, self.Src_bgn(), self.Src_end()); 
-		switch (error) {
-			case  -1:	// empty coord; EX: {{#deg2dd:|precision=6}}}} PAGE:it.v:Sami; DATE:2014-03-02
-			case  -2:	// words > 4; EX:{{#geoLink: $1 $2 $3 $4 $5 $6|lat=51°20′00″19°55′50″}}; PAGE:pl.v:Rezerwat_przyrody_Jaksonek DATE:2014-08-14
-			case  -3:	// invalid delim; EX:{{#geoLink: $1 $2 $3 $4 $5 $6|lat=51°31′37″|long=20°13′17'}}; PAGE:pl.v:Rezerwat_przyrody_Ciosny DATE:2014-08-14
-				ctx.App().Usr_dlg().Log_many("", "", "mapSources failed: page=~{0} pfunc=~{1} err=~{2} src=~{3}", page_str, pfunc_name_str, error, self_str); // don't warn b/c there are many
-				break;
-			default:
-				ctx.App().Usr_dlg().Warn_many("", "", "mapSources failed: page=~{0} pfunc=~{1} err=~{2} src=~{3}", page_str, pfunc_name_str, error, self_str);
-				break;
-		}
-	}
-	private void New_coord(byte[] input, byte[] dir, int prec) {	// REF.MW:newCoord
-		this.error = 0; this.word_idx_nsew = -1;
-		coord_dec = coord_deg = coord_min = coord_sec = 0;
-		rv[0] = rv[1] = rv[2] = 0; rv[3] = 1;
-		this.dir_id = Parse_dir(dir);
-		this.prec = Parse_precision(prec);
-		this.dec = 0;
-		this.step = 0;
-		Parse_input(input);
-	}
-	private void Set_coord() {	// REF.MW:setCoord
-		if (step > 1) return;
-		if (prec < 9)
-			dec = Math_.Round(dec, prec);
-		int sign = dec < 0 ? -1 : 1;
-		double angle = Math_.Abs_double(dec);
-		double deg = Math_.Floor(angle);
-		double min = (angle - deg) * 60;
-		double sec = prec > 0
-			? Math_.Round((min - Math_.Floor(min)) * 60, prec - 4)
-			: Math_.Round((min - Math_.Floor(min)) * 60, 0)
-			;
-		min = Math_.Floor(min);
-		if (sec >= 60) {
-			sec -= 60;
-			min++;
-		}
-		if (prec < 3 && sec >= 30)
-			min++;
-		if (prec < 3)
-			sec = 0;
-		if (min >= 60) {
-			min -= 60;
-			deg++;
-		}
-		if (prec < 1 && min >= 30) {
-			deg++;
-		}
-		if (prec < 1)
-			min = 0;
-		coord_dec = Math_.Round(dec, prec);
-		coord_deg = deg * sign;
-		coord_min = min;
-		coord_sec = sec;
-		if (sign > 0) {
-			coord_dir_ns = Compass_N;
-			coord_dir_ew = Compass_E;
-		}
-		else {
-			coord_dir_ns = Compass_S;
-			coord_dir_ew = Compass_W;
-		}
-		step = 2;
-	}
-	public byte[] Get_dms(boolean wikibase, byte[] plus, byte[] minus) { // REF.MW: getDMSString
-		if (step < 2) Set_coord();
-		double deg = coord_deg;
-		if (	dec < 0 
-			&& (	(Bry_.Len_gt_0(plus) || Bry_.Len_gt_0(minus))
-				||	wikibase	// NOTE: wikibase will always pass in empty plus / minus; still need to suppress "-" sign because letter has already been reversed; EX:"-2 E" -> "2 W" x> "-2 W" DATE:2017-04-02
-				)
-			) {
-			deg = Math_.Abs_double(deg);
-		}
-		tmp_bfr.Add_double(deg).Add(Bry_deg);
-		if (prec > 0) {
-			if (!wikibase) // NOTE: do not add space if wikibase, else will fail in Module:en.w:WikidataCoord; PAGE:en.w:Hulme_Arch_Bridge DATE:2017-04-02
-				tmp_bfr.Add_byte_space();
-			tmp_bfr.Add_double(coord_min).Add(wikibase ? Bry_apos_wb : Bry_apos_mw);
-		}
-		if (prec > 2) {
-			if (!wikibase)	// NOTE: do not add space if wikibase, else will fail in Module:en.w:WikidataCoord; PAGE:en.w:Hulme_Arch_Bridge DATE:2017-04-02
-				tmp_bfr.Add_byte_space();
-			tmp_bfr.Add_double(coord_sec).Add(wikibase ? Bry_quot_wb : Bry_quot_mw);
-		}
-		byte[] letter = null;
-		if (dir_id == Dir_lat_id)
-			letter = coord_dir_ns;
-		if (dir_id == Dir_long_id)
-			letter = coord_dir_ew;
-		if (dec > 0 && Bry_.Len_gt_0(plus))
-			letter = plus;
-		if (dec < 0 && Bry_.Len_gt_0(minus))
-			letter = minus;
-		if (letter != null) {
-			tmp_bfr.Add_byte_space();
-			tmp_bfr.Add(letter);
-		}
-		return tmp_bfr.To_bry_and_clear();
-	}
-	private void Parse_input(byte[] src) {	// REF.MW: toDec
-		src = Parse_input_normalize(tmp_bfr, src);
-		if (src == null) {error = -1; return;}
-		int src_len = src.length;
-		int word_idx = -1, word_bgn = 0, words_len = 0;
-		int i = 0;			
-		while (true) {
-			boolean is_last = i == src_len;
-			byte b = is_last ? Byte_ascii.Space : src[i];
-			switch (b) {
-				case Byte_ascii.Space:
-					Parse_input_word(rv, src, ++word_idx, word_bgn, i);
-					++words_len;
-					i = Bry_find_.Find_fwd_while_space_or_tab(src, i, src_len);
-					word_bgn = i;
-					break;
-			}
-			if (is_last) break;
-			i++;
-		}
-		if (words_len < 1 || words_len > 4) {error = -2; return;}
-		if (word_idx_nsew != -1 && word_idx_nsew != words_len - 1) {error = -10; return;}
-		if (rv[0] >= 0)
-			dec = (rv[0] + rv[1] / 60 + rv[2] / 3600 ) * rv[3];
-		else
-			dec = (rv[0] - rv[1] / 60 - rv[2] / 3600 ) * rv[3];
-		this.step = 1;
-	}
-	private boolean Parse_input_word_is_compass(byte v) {
-		switch (v) {
-			case Byte_ascii.Ltr_N:
-			case Byte_ascii.Ltr_E:
-			case Byte_ascii.Ltr_S:
-			case Byte_ascii.Ltr_W:
-				return true;
-			default:
-				return false;
-		}
-	}
-	private void Parse_input_word(double[] rv, byte[] input, int word_idx, int word_bgn, int word_end) {
-		if (word_idx >= Input_units_len) return;
-		byte unit_dlm = Input_units[word_idx];
-		int pos = Bry_find_.Find_fwd(input, unit_dlm, word_bgn, word_end);
-		if (pos != Bry_find_.Not_found)	// remove dlms from end of bry; EX: "123'" -> "123"
-			word_end = pos;
-		if (!Parse_input_word_is_compass(input[word_bgn])) {	// if ( is_numeric( $v ) ) {
-			double word_val = Bry_.To_double_or(input, word_bgn, word_end, Double_.NaN);
-			if (!Double_.IsNaN(word_val)) {
-				if (word_idx > 2) {error = -4; return;}
-				switch (word_idx) {
-					case 0:
-						if (word_val <= -180 || word_val > 180) {error = -5; return;}
-						rv[0] = word_val;
-						break;
-					case 1:
-						if (word_val < 0 || word_val >= 60) {error = -6; return;}
-						if (rv[0] != (int)(rv[0])) {error = -7; return;}
-						rv[1] = word_val;
-						break;
-					case 2:
-						if (word_val < 0 || word_val >= 60) {error = -8; return;}
-						if (rv[1] != (int)(rv[1])) {error = -9; return;}
-						rv[2] = word_val;
-						break;
-				}
-			}
-			else {
-				error = -3;
-				return;
-			}
-		}
-		else {	// 'NSEW'
-			word_idx_nsew = word_idx;
-			byte word_byte = input[word_bgn];
-			if (rv[0] < 0) {error = -11; return;}
-			if (word_end - word_bgn != 1) {error = -3; return;}
-			switch (dir_id) {
-				case Dir_long_id:
-					if (word_byte == Byte_ascii.Ltr_N || word_byte == Byte_ascii.Ltr_S) {error = -12; return;}
-					break;
-				case Dir_lat_id:
-					if (word_byte == Byte_ascii.Ltr_E || word_byte == Byte_ascii.Ltr_W) {error = -12; return;}
-					break;
-				case Dir_unknown_id:
-					if (word_byte == Byte_ascii.Ltr_N || word_byte == Byte_ascii.Ltr_S)	this.dir_id = Dir_lat_id;
-					else																this.dir_id = Dir_long_id;
-					break;
-			}
-			if (this.dir_id == Dir_lat_id) {
-				double rv_0 = rv[0];
-				if (rv_0 < -90 || rv_0 > 90) {error = -13; return;}
-			}
-			if (word_byte == Byte_ascii.Ltr_S || word_byte == Byte_ascii.Ltr_W)
-				rv[3] = -1;
-		}
-	}
-	private static byte Parse_dir(byte[] dir) {
-		if (Bry_.Len_eq_0(dir)) return Dir_unknown_id;
-		Object dir_obj = Dir_trie.Match_bgn(dir, 0, dir.length);
-		return dir_obj == null ? Dir_unknown_id : ((Byte_obj_val)dir_obj).Val();
-	}
-	private static int Parse_precision(int val) {	// REF.MW: MapSourcesMath.php|newCoord
-		if		(val > -1 && val < 10)		return val;
-		else if	(val == -1)					return 9;
-		else								return 4;
-	}
-	private Bry_bfr tmp_bfr = Bry_bfr_.Reset(32);
-	public static byte[] Parse_input_normalize(Bry_bfr bfr, byte[] src) {
-		/*
-		$w = str_replace( array( '‘', '’', '′' ), "'", $input );
-		$w = str_replace( array( "''", '“', '”', '″' ), '"', $w );
-		$w = str_replace( '−', '-', $w );
-		$w = strtoupper( str_replace( array( '_', '/', "\t", "\n", "\r" ), ' ', $w ) );
-		$w = str_replace( array( '°', "'", '"' ), array( '° ', "' ", '" ' ), $w );
-		$w = trim( str_replace( array( 'N', 'S', 'E', 'W' ), array( ' N', ' S', ' E', ' W' ), $w ) );
-		*/
-		int src_end = src.length; if (src_end == 0) return null;			
-		src = Trie__normalize__apos.Replace(bfr, src, 0, src_end);		// normalize apos separately, since 2 apos can go to quotes; EX: ‘’ -> "; PAGE:it.v:Morro_d'Oro DATE:2015-12-06
-		src = Trie__normalize__rest.Replace(bfr, src, 0, src.length);	// normalize rest;
-		return Bry_.Trim(src);
-	}
-	private static final    byte[] 
-	  Bry_deg = Bry_.new_u8("°")
-	, Bry_quot_mw = Bry_.new_a7("&quot;")
-	, Bry_quot_wb = Bry_.new_a7("&#34;") // REF:en.w:Module:WikidataCoord
-	, Bry_apos_mw = Bry_.new_a7("'")
-	, Bry_apos_wb = Bry_.new_a7("&#39;") // REF:en.w:Module:WikidataCoord
-	;
-	private static final byte Dir_unknown_id = 0, Dir_lat_id = 1, Dir_long_id = 2;
-	public static final    byte[] Dir_lat_bry = Bry_.new_a7("lat"), Dir_long_bry = Bry_.new_a7("long");
-	private static final    Btrie_slim_mgr Dir_trie = Btrie_slim_mgr.ci_a7()	// NOTE:ci.ascii:MW_const.en
-	.Add_bry_byte(Dir_lat_bry			, Dir_lat_id)
-	.Add_bry_byte(Dir_long_bry			, Dir_long_id)
-	;
-	private static final    byte[] 
-	  Compass_N = new byte[] {Byte_ascii.Ltr_N}
-	, Compass_E = new byte[] {Byte_ascii.Ltr_E}
-	, Compass_S = new byte[] {Byte_ascii.Ltr_S}
-	, Compass_W = new byte[] {Byte_ascii.Ltr_W}
-	;
-	private static final byte Input_byte_degree = Byte_ascii.Slash;	// NOTE: ugly cheat to avoid using multi-byte char; note that all "/" are swapped out to " ", so any remaining "/" was added by the normalizer; EX:  "123° 4/5" -> "123/ 4 5"
-	private static final    byte[] Input_units = new byte[] {Input_byte_degree, Byte_ascii.Apos, Byte_ascii.Quote, Byte_ascii.Space};
-	private static final    int Input_units_len = Input_units.length;
-	private static final    Btrie_slim_mgr Trie__normalize__apos = Btrie_slim_mgr.cs()
-	.Add_replace_many	(Byte_ascii.Apos_bry	, "‘", "’", "′");
-	private static final    Btrie_slim_mgr Trie__normalize__rest = Btrie_slim_mgr.cs()
-	.Add_replace_many	("' "					, "'")
-	.Add_replace_many	("\" "					, "\"", "''", "“", "”", "″")
-	.Add_replace_many	(Byte_ascii.Dash_bry	, "-", "−")							// NOTE: emdash and endash
-	.Add_replace_many	(Byte_ascii.Space_bry	, " ", "_", "/", "\t", "\n", "\r") 	// NOTE: " " = &nbsp;
-	.Add_replace_many	("/ "					, "°")
-	.Add_replace_many	(" N"					, "N", "n")
-	.Add_replace_many	(" S"					, "S", "s")
-	.Add_replace_many	(" E"					, "E", "e")
-	.Add_replace_many	(" W"					, "W", "w");
-	public static final    Map_math Instance = new Map_math();
-}
+package gplx.xowa.xtns.mapSources;
+
+import gplx.Bry_;
+import gplx.Bry_bfr;
+import gplx.Bry_bfr_;
+import gplx.Bry_find_;
+import gplx.Byte_ascii;
+import gplx.Double_;
+import gplx.Err_;
+import gplx.Math_;
+import gplx.String_;
+import gplx.core.btries.Btrie_slim_mgr;
+import gplx.core.primitives.Byte_obj_val;
+import gplx.xowa.parsers.Xop_ctx;
+import gplx.xowa.parsers.tmpls.Xot_invk;
+
+class Map_math {// REF.MW:MapSources_math.php
+	private int word_idx_nsew;
+	private double[] rv = new double[4];
+	private byte dir_id;
+	private int prec;
+	private int step;
+	public double Dec() {return dec;} private double dec;
+	public int Error() {return error;} private int error;
+	public double Coord_dec() {return coord_dec;} private double coord_dec;
+	public double Coord_deg() {return coord_deg;} private double coord_deg;
+	public double Coord_min() {return coord_min;} private double coord_min;
+	public double Coord_sec() {return coord_sec;} private double coord_sec;
+	public byte[] Coord_dir_ns() {return coord_dir_ns;} private byte[] coord_dir_ns;
+	public byte[] Coord_dir_ew() {return coord_dir_ew;} private byte[] coord_dir_ew;
+	public boolean Ctor(byte[] input, int prec, byte[] dir, int until_step) {
+		try {
+			New_coord(input, dir, prec);
+			if (until_step > 1 && error == 0)
+				Set_coord();
+		} catch (Exception e) {
+			Err_.Noop(e);
+			error = -128;
+		}
+		return error == 0;
+	}
+	public void Fail(Xop_ctx ctx, byte[] src, Xot_invk self, Bry_bfr bfr, byte[] pfunc_name) {
+		String page_str = ctx.Page().Url().To_str();
+		String pfunc_name_str = String_.new_u8(pfunc_name);
+		String self_str = String_.new_u8(src, self.Src_bgn(), self.Src_end()); 
+		switch (error) {
+			case  -1:	// empty coord; EX: {{#deg2dd:|precision=6}}}} PAGE:it.v:Sami; DATE:2014-03-02
+			case  -2:	// words > 4; EX:{{#geoLink: $1 $2 $3 $4 $5 $6|lat=51°20′00″19°55′50″}}; PAGE:pl.v:Rezerwat_przyrody_Jaksonek DATE:2014-08-14
+			case  -3:	// invalid delim; EX:{{#geoLink: $1 $2 $3 $4 $5 $6|lat=51°31′37″|long=20°13′17'}}; PAGE:pl.v:Rezerwat_przyrody_Ciosny DATE:2014-08-14
+				ctx.App().Usr_dlg().Log_many("", "", "mapSources failed: page=~{0} pfunc=~{1} err=~{2} src=~{3}", page_str, pfunc_name_str, error, self_str); // don't warn b/c there are many
+				break;
+			default:
+				ctx.App().Usr_dlg().Warn_many("", "", "mapSources failed: page=~{0} pfunc=~{1} err=~{2} src=~{3}", page_str, pfunc_name_str, error, self_str);
+				break;
+		}
+	}
+	private void New_coord(byte[] input, byte[] dir, int prec) {	// REF.MW:newCoord
+		this.error = 0; this.word_idx_nsew = -1;
+		coord_dec = coord_deg = coord_min = coord_sec = 0;
+		rv[0] = rv[1] = rv[2] = 0; rv[3] = 1;
+		this.dir_id = Parse_dir(dir);
+		this.prec = Parse_precision(prec);
+		this.dec = 0;
+		this.step = 0;
+		Parse_input(input);
+	}
+	private void Set_coord() {	// REF.MW:setCoord
+		if (step > 1) return;
+		if (prec < 9)
+			dec = Math_.Round(dec, prec);
+		int sign = dec < 0 ? -1 : 1;
+		double angle = Math_.Abs_double(dec);
+		double deg = Math_.Floor(angle);
+		double min = (angle - deg) * 60;
+		double sec = prec > 4 // 2020-09-03|ISSUE#:792|precision check should be > 4 not > 0;PAGE:en.w:Huntington_Plaza
+			? Math_.Round((min - Math_.Floor(min)) * 60, prec - 4)
+			: Math_.Round((min - Math_.Floor(min)) * 60, 0)
+			;
+		min = Math_.Floor(min);
+		if (sec >= 60) {
+			sec -= 60;
+			min++;
+		}
+		if (prec < 3 && sec >= 30)
+			min++;
+		if (prec < 3)
+			sec = 0;
+		if (min >= 60) {
+			min -= 60;
+			deg++;
+		}
+		if (prec < 1 && min >= 30) {
+			deg++;
+		}
+		if (prec < 1)
+			min = 0;
+		coord_dec = Math_.Round(dec, prec);
+		coord_deg = deg * sign;
+		coord_min = min;
+		coord_sec = sec;
+		if (sign > 0) {
+			coord_dir_ns = Compass_N;
+			coord_dir_ew = Compass_E;
+		}
+		else {
+			coord_dir_ns = Compass_S;
+			coord_dir_ew = Compass_W;
+		}
+		step = 2;
+	}
+	public byte[] Get_dms(boolean wikibase, byte[] plus, byte[] minus) { // REF.MW: getDMSString
+		if (step < 2) Set_coord();
+		double deg = coord_deg;
+		if (	dec < 0 
+			&& (	(Bry_.Len_gt_0(plus) || Bry_.Len_gt_0(minus))
+				||	wikibase	// NOTE: wikibase will always pass in empty plus / minus; still need to suppress "-" sign because letter has already been reversed; EX:"-2 E" -> "2 W" x> "-2 W" DATE:2017-04-02
+				)
+			) {
+			deg = Math_.Abs_double(deg);
+		}
+		tmp_bfr.Add_double(deg).Add(Bry_deg);
+		if (prec > 0) {
+			if (!wikibase) // NOTE: do not add space if wikibase, else will fail in Module:en.w:WikidataCoord; PAGE:en.w:Hulme_Arch_Bridge DATE:2017-04-02
+				tmp_bfr.Add_byte_space();
+			tmp_bfr.Add_double(coord_min).Add(wikibase ? Bry_apos_wb : Bry_apos_mw);
+		}
+		if (prec > 2) {
+			if (!wikibase)	// NOTE: do not add space if wikibase, else will fail in Module:en.w:WikidataCoord; PAGE:en.w:Hulme_Arch_Bridge DATE:2017-04-02
+				tmp_bfr.Add_byte_space();
+			tmp_bfr.Add_double(coord_sec).Add(wikibase ? Bry_quot_wb : Bry_quot_mw);
+		}
+		byte[] letter = null;
+		if (dir_id == Dir_lat_id)
+			letter = coord_dir_ns;
+		if (dir_id == Dir_long_id)
+			letter = coord_dir_ew;
+		if (dec > 0 && Bry_.Len_gt_0(plus))
+			letter = plus;
+		if (dec < 0 && Bry_.Len_gt_0(minus))
+			letter = minus;
+		if (letter != null) {
+			tmp_bfr.Add_byte_space();
+			tmp_bfr.Add(letter);
+		}
+		return tmp_bfr.To_bry_and_clear();
+	}
+	private void Parse_input(byte[] src) {	// REF.MW: toDec
+		src = Parse_input_normalize(tmp_bfr, src);
+		if (src == null) {error = -1; return;}
+		int src_len = src.length;
+		int word_idx = -1, word_bgn = 0, words_len = 0;
+		int i = 0;			
+		while (true) {
+			boolean is_last = i == src_len;
+			byte b = is_last ? Byte_ascii.Space : src[i];
+			switch (b) {
+				case Byte_ascii.Space:
+					Parse_input_word(rv, src, ++word_idx, word_bgn, i);
+					++words_len;
+					i = Bry_find_.Find_fwd_while_space_or_tab(src, i, src_len);
+					word_bgn = i;
+					break;
+			}
+			if (is_last) break;
+			i++;
+		}
+		if (words_len < 1 || words_len > 4) {error = -2; return;}
+		if (word_idx_nsew != -1 && word_idx_nsew != words_len - 1) {error = -10; return;}
+		if (rv[0] >= 0)
+			dec = (rv[0] + rv[1] / 60 + rv[2] / 3600 ) * rv[3];
+		else
+			dec = (rv[0] - rv[1] / 60 - rv[2] / 3600 ) * rv[3];
+		this.step = 1;
+	}
+	private boolean Parse_input_word_is_compass(byte v) {
+		switch (v) {
+			case Byte_ascii.Ltr_N:
+			case Byte_ascii.Ltr_E:
+			case Byte_ascii.Ltr_S:
+			case Byte_ascii.Ltr_W:
+				return true;
+			default:
+				return false;
+		}
+	}
+	private void Parse_input_word(double[] rv, byte[] input, int word_idx, int word_bgn, int word_end) {
+		if (word_idx >= Input_units_len) return;
+		byte unit_dlm = Input_units[word_idx];
+		int pos = Bry_find_.Find_fwd(input, unit_dlm, word_bgn, word_end);
+		if (pos != Bry_find_.Not_found)	// remove dlms from end of bry; EX: "123'" -> "123"
+			word_end = pos;
+		if (!Parse_input_word_is_compass(input[word_bgn])) {	// if ( is_numeric( $v ) ) {
+			double word_val = Bry_.To_double_or(input, word_bgn, word_end, Double_.NaN);
+			if (!Double_.IsNaN(word_val)) {
+				if (word_idx > 2) {error = -4; return;}
+				switch (word_idx) {
+					case 0:
+						if (word_val <= -180 || word_val > 180) {error = -5; return;}
+						rv[0] = word_val;
+						break;
+					case 1:
+						if (word_val < 0 || word_val >= 60) {error = -6; return;}
+						if (rv[0] != (int)(rv[0])) {error = -7; return;}
+						rv[1] = word_val;
+						break;
+					case 2:
+						if (word_val < 0 || word_val >= 60) {error = -8; return;}
+						if (rv[1] != (int)(rv[1])) {error = -9; return;}
+						rv[2] = word_val;
+						break;
+				}
+			}
+			else {
+				error = -3;
+				return;
+			}
+		}
+		else {	// 'NSEW'
+			word_idx_nsew = word_idx;
+			byte word_byte = input[word_bgn];
+			if (rv[0] < 0) {error = -11; return;}
+			if (word_end - word_bgn != 1) {error = -3; return;}
+			switch (dir_id) {
+				case Dir_long_id:
+					if (word_byte == Byte_ascii.Ltr_N || word_byte == Byte_ascii.Ltr_S) {error = -12; return;}
+					break;
+				case Dir_lat_id:
+					if (word_byte == Byte_ascii.Ltr_E || word_byte == Byte_ascii.Ltr_W) {error = -12; return;}
+					break;
+				case Dir_unknown_id:
+					if (word_byte == Byte_ascii.Ltr_N || word_byte == Byte_ascii.Ltr_S)	this.dir_id = Dir_lat_id;
+					else																this.dir_id = Dir_long_id;
+					break;
+			}
+			if (this.dir_id == Dir_lat_id) {
+				double rv_0 = rv[0];
+				if (rv_0 < -90 || rv_0 > 90) {error = -13; return;}
+			}
+			if (word_byte == Byte_ascii.Ltr_S || word_byte == Byte_ascii.Ltr_W)
+				rv[3] = -1;
+		}
+	}
+	private static byte Parse_dir(byte[] dir) {
+		if (Bry_.Len_eq_0(dir)) return Dir_unknown_id;
+		Object dir_obj = Dir_trie.Match_bgn(dir, 0, dir.length);
+		return dir_obj == null ? Dir_unknown_id : ((Byte_obj_val)dir_obj).Val();
+	}
+	private static int Parse_precision(int val) {	// REF.MW: MapSourcesMath.php|newCoord
+		if		(val > -1 && val < 10)		return val;
+		else if	(val == -1)					return 9;
+		else								return 4;
+	}
+	private Bry_bfr tmp_bfr = Bry_bfr_.Reset(32);
+	public static byte[] Parse_input_normalize(Bry_bfr bfr, byte[] src) {
+		/*
+		$w = str_replace( array( '‘', '’', '′' ), "'", $input );
+		$w = str_replace( array( "''", '“', '”', '″' ), '"', $w );
+		$w = str_replace( '−', '-', $w );
+		$w = strtoupper( str_replace( array( '_', '/', "\t", "\n", "\r" ), ' ', $w ) );
+		$w = str_replace( array( '°', "'", '"' ), array( '° ', "' ", '" ' ), $w );
+		$w = trim( str_replace( array( 'N', 'S', 'E', 'W' ), array( ' N', ' S', ' E', ' W' ), $w ) );
+		*/
+		int src_end = src.length; if (src_end == 0) return null;			
+		src = Trie__normalize__apos.Replace(bfr, src, 0, src_end);		// normalize apos separately, since 2 apos can go to quotes; EX: ‘’ -> "; PAGE:it.v:Morro_d'Oro DATE:2015-12-06
+		src = Trie__normalize__rest.Replace(bfr, src, 0, src.length);	// normalize rest;
+		return Bry_.Trim(src);
+	}
+	private static final    byte[] 
+	  Bry_deg = Bry_.new_u8("°")
+	, Bry_quot_mw = Bry_.new_a7("&quot;")
+	, Bry_quot_wb = Bry_.new_a7("&#34;") // REF:en.w:Module:WikidataCoord
+	, Bry_apos_mw = Bry_.new_a7("'")
+	, Bry_apos_wb = Bry_.new_a7("&#39;") // REF:en.w:Module:WikidataCoord
+	;
+	private static final byte Dir_unknown_id = 0, Dir_lat_id = 1, Dir_long_id = 2;
+	public static final    byte[] Dir_lat_bry = Bry_.new_a7("lat"), Dir_long_bry = Bry_.new_a7("long");
+	private static final    Btrie_slim_mgr Dir_trie = Btrie_slim_mgr.ci_a7()	// NOTE:ci.ascii:MW_const.en
+	.Add_bry_byte(Dir_lat_bry			, Dir_lat_id)
+	.Add_bry_byte(Dir_long_bry			, Dir_long_id)
+	;
+	private static final    byte[] 
+	  Compass_N = new byte[] {Byte_ascii.Ltr_N}
+	, Compass_E = new byte[] {Byte_ascii.Ltr_E}
+	, Compass_S = new byte[] {Byte_ascii.Ltr_S}
+	, Compass_W = new byte[] {Byte_ascii.Ltr_W}
+	;
+	private static final byte Input_byte_degree = Byte_ascii.Slash;	// NOTE: ugly cheat to avoid using multi-byte char; note that all "/" are swapped out to " ", so any remaining "/" was added by the normalizer; EX:  "123° 4/5" -> "123/ 4 5"
+	private static final    byte[] Input_units = new byte[] {Input_byte_degree, Byte_ascii.Apos, Byte_ascii.Quote, Byte_ascii.Space};
+	private static final    int Input_units_len = Input_units.length;
+	private static final    Btrie_slim_mgr Trie__normalize__apos = Btrie_slim_mgr.cs()
+	.Add_replace_many	(Byte_ascii.Apos_bry	, "‘", "’", "′");
+	private static final    Btrie_slim_mgr Trie__normalize__rest = Btrie_slim_mgr.cs()
+	.Add_replace_many	("' "					, "'")
+	.Add_replace_many	("\" "					, "\"", "''", "“", "”", "″")
+	.Add_replace_many	(Byte_ascii.Dash_bry	, "-", "−")							// NOTE: emdash and endash
+	.Add_replace_many	(Byte_ascii.Space_bry	, " ", "_", "/", "\t", "\n", "\r") 	// NOTE: " " = &nbsp;
+	.Add_replace_many	("/ "					, "°")
+	.Add_replace_many	(" N"					, "N", "n")
+	.Add_replace_many	(" S"					, "S", "s")
+	.Add_replace_many	(" E"					, "E", "e")
+	.Add_replace_many	(" W"					, "W", "w");
+	public static final    Map_math Instance = new Map_math();
+}

+ 205 - 200
400_xowa/src/gplx/xowa/xtns/wbases/pfuncs/Wdata_pf_property__basic__tst.java

@@ -1,6 +1,6 @@
 /*
 XOWA: the XOWA Offline Wiki Application
-Copyright (C) 2012-2017 gnosygnu@gmail.com
+Copyright (C) 2012-2020 gnosygnu@gmail.com
 
 XOWA is licensed under the terms of the General Public License (GPL) Version 3,
 or alternatively under the terms of the Apache License Version 2.0.
@@ -13,202 +13,207 @@ The terms of each license can be found in the source code repository:
 GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
 Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
-package gplx.xowa.xtns.wbases.pfuncs; import gplx.*; import gplx.xowa.*; import gplx.xowa.xtns.*; import gplx.xowa.xtns.wbases.*;
-import org.junit.*; import gplx.xowa.parsers.*; import gplx.xowa.parsers.tmpls.*;
-public class Wdata_pf_property__basic__tst {
-	@Before public void init() {fxt.Init();} private final    Wdata_wiki_mgr_fxt fxt = new Wdata_wiki_mgr_fxt();
-	@Test   public void String() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-		fxt.Test_parse("{{#property:p1}}", "a");
-		fxt.Test_parse("{{#property:p2}}", "");
-	}
-	@Test   public void Entity() {
-		fxt.Init__docs__add(fxt.Wdoc("Q2")
-			.Add_label("en", "b")
-			);
-		fxt.Init__docs__add(fxt.Wdoc("Q1")
-			.Add_claims(fxt.Make_claim_entity_qid(1, 2))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-		fxt.Test_parse("{{#property:p1}}", "b");
-	}
-	@Test   public void Entity_fr() {	// PURPOSE: non-English wiki should default to English label if non-English label not available; DATE:2013-12-19
-		// set wiki to French
-		fxt.Wiki().Wdata_wiki_lang_(Bry_.new_a7("fr"));
-
-		fxt.Init__docs__add(fxt.Wdoc("Q1")
-			.Add_claims(fxt.Make_claim_entity_qid(1, 2))
-			.Add_sitelink("frwiki", "Test_page")
-			);
-
-		// create wdata page Q2 with label in en (not fr)
-		fxt.Init__docs__add(fxt.Wdoc("Q2")
-			.Add_label("en", "b")
-			);
-
-		// parse; should get en label
-		fxt.Test_parse("{{#property:p1}}", "b");									
-	}
-	@Test   public void Entity_missing() {	// PURPOSE: wiki may refer to entity that no longer exists; EX: {{#property:p1}} which links to Q1, but p1 links to Q2 and Q2 was deleted; DATE:2014-02-01
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_entity_qid(1, 2)) // create wdata page Q1 with prop entity reference to Q2; note that Q2 is not created
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "");										// parse; get ""
-	}
-	@Test   public void Time() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_time(1, "2012-01-02 03:04:05"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "30405 2 Jan 2012");	// NOTE: format is missing ":" b/c test does not init messages for html_wtr;  DATE:2015-08-03
-	}
-	@Test   public void Geodata() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_geo(1, "6.789", "1.2345"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1°14&#39;4.2&#34;N, 6°47&#39;20.4&#34;E");
-	}
-	@Test   public void Quantity__plus_minus__y() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1236", "+1232"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1,234±2");
-	}
-	@Test   public void Quantity__plus_minus__n() {	// PURPOSE:do not output ± if lbound == val == ubound; PAGE:en.w:Tintinan DATE:2015-08-02
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1234", "+1234"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1,234");
-	}
-	@Test   public void Quantity__range() {	// PURPOSE:do not output ± if lbound == val == ubound; PAGE:en.w:Tintinan DATE:2015-08-02
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1236", "+1233"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1,233-1,236");
-	}
-	@Test   public void Quantity__long() {	// PURPOSE: must cast to long for large numbers; EX:{{#property:P1082}} PAGE:en.w:Earth; DATE:2015-08-02
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+4321000000", "1", "4321000000", "4321000000"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "4,321,000,000");
-	}
-	@Test   public void Quantity__unit__entity() {// PURPOSE: get entity name; EX:{{#invoke:Wikidata|getUnits|P2386|FETCH_WIKIDATA}} PAGE:en.w:Arecibo_Observatory; DATE:2016-10-11
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "http://www.wikidata.org/entity/Q2", "+1236", "+1232"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Init__docs__add(fxt.Wdoc("Q2")				
-			.Add_claims(fxt.Make_claim_string(2, "a"))
-			.Add_label("en", "meter")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1,234±2 meter");
-	}
-	@Test   public void Quantity__decimal() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_quantity(1, "+1234.50", "1", "+1236.75", "+1232.25"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "1,234.5±2.25");
-	}
-	@Test   public void Monolingualtext() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_monolingual(1, "la", "Lorem ipsum dolor sit amet"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "Lorem ipsum dolor sit amet");
-	}
-	@Test   public void Novalue() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_novalue(1))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "no value");
-	}
-	@Test   public void Somevalue() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_somevalue(1))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "unknown value");
-	}
-	@Test   public void Multiple() {
-		fxt.Init__docs__add(fxt.Wdoc("Q1")				
-			.Add_claims(fxt.Make_claim_string(1, "a"), fxt.Make_claim_string(1, "b"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1}}", "a");	// only take first; DATE:2015-08-02
-	}
-	@Test   public void Q() {
-		fxt.Init__docs__add(fxt.Wdoc("Q2")				
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-
-		fxt.Test_parse("{{#property:p1|q=Q2}}", "a");
-	}
-	@Test   public void Of() {
-		fxt.Init__docs__add(fxt.Wdoc("Q2")				
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			.Add_sitelink("enwiki", "Of_page")
-			);
-
-		fxt.Test_parse("{{#property:p1|of=Of_page}}", "a");
-	}
-	@Test   public void From() {
-		fxt.Init__docs__add(fxt.Wdoc("Property:P2")
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			);
-
-		fxt.Test_parse("{{#property:p1|from=P2}}", "a");
-		fxt.Test_parse("{{#property:p1|from=}}", "");
-		fxt.Test_parse("{{#property:p1| from = P2 }}", "a"); // PURPOSE: trim ws; ISSUE#:361; DATE:2019-02-11
-	}
-	@Test   public void Pid_as_name() {
-		fxt.Init__docs__add(fxt.Wdoc("Q2")
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-		fxt.Init_pids_add("en", "astronomic symbol", 1);
-
-		fxt.Test_parse("{{#property:astronomic symbol}}", "a");
-	}
-	@Test   public void Empty_arg() {	// PURPOSE: {{#property:p1|}} should not fail / warn; DATE:2013-11-15
-		fxt.Init__docs__add(fxt.Wdoc("Q2")
-			.Add_claims(fxt.Make_claim_string(1, "a"))
-			.Add_sitelink("enwiki", "Test_page")
-			);
-		fxt.Init_pids_add("en", "astronomic symbol", 1);
-
-		fxt.Test_parse("{{#property:p1|}}", "a");
-	}
-	@Test   public void Parse_pid() {
-		fxt.Test_parse_pid		("p123"	, 123);		// basic
-		fxt.Test_parse_pid		("P123"	, 123);		// uppercase
-		fxt.Test_parse_pid_null	("population");		// name test
-		fxt.Test_parse_pid_null	("123");			// missing p
-		fxt.Test_parse_pid_null	("");				// empty String test
-	}
-}
+package gplx.xowa.xtns.wbases.pfuncs;
+
+import gplx.Bry_;
+import gplx.xowa.xtns.wbases.Wdata_wiki_mgr_fxt;
+import org.junit.Before;
+import org.junit.Test;
+
+public class Wdata_pf_property__basic__tst {
+	@Before public void init() {fxt.Init();} private final Wdata_wiki_mgr_fxt fxt = new Wdata_wiki_mgr_fxt();
+	@Test public void String() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+		fxt.Test_parse("{{#property:p1}}", "a");
+		fxt.Test_parse("{{#property:p2}}", "");
+	}
+	@Test public void Entity() {
+		fxt.Init__docs__add(fxt.Wdoc("Q2")
+			.Add_label("en", "b")
+			);
+		fxt.Init__docs__add(fxt.Wdoc("Q1")
+			.Add_claims(fxt.Make_claim_entity_qid(1, 2))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+		fxt.Test_parse("{{#property:p1}}", "b");
+	}
+	@Test public void Entity_fr() {	// PURPOSE: non-English wiki should default to English label if non-English label not available; DATE:2013-12-19
+		// set wiki to French
+		fxt.Wiki().Wdata_wiki_lang_(Bry_.new_a7("fr"));
+
+		fxt.Init__docs__add(fxt.Wdoc("Q1")
+			.Add_claims(fxt.Make_claim_entity_qid(1, 2))
+			.Add_sitelink("frwiki", "Test_page")
+			);
+
+		// create wdata page Q2 with label in en (not fr)
+		fxt.Init__docs__add(fxt.Wdoc("Q2")
+			.Add_label("en", "b")
+			);
+
+		// parse; should get en label
+		fxt.Test_parse("{{#property:p1}}", "b");									
+	}
+	@Test public void Entity_missing() {	// PURPOSE: wiki may refer to entity that no longer exists; EX: {{#property:p1}} which links to Q1, but p1 links to Q2 and Q2 was deleted; DATE:2014-02-01
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_entity_qid(1, 2)) // create wdata page Q1 with prop entity reference to Q2; note that Q2 is not created
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "");										// parse; get ""
+	}
+	@Test public void Time() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_time(1, "2012-01-02 03:04:05"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "30405 2 Jan 2012");	// NOTE: format is missing ":" b/c test does not init messages for html_wtr;  DATE:2015-08-03
+	}
+	@Test public void Geodata() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_geo(1, "6.789", "1.2345"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1°14&#39;4.2&#34;N, 6°47&#39;20.4&#34;E");
+	}
+	@Test public void Quantity__plus_minus__y() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1236", "+1232"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1,234±2");
+	}
+	@Test public void Quantity__plus_minus__n() {	// PURPOSE:do not output ± if lbound == val == ubound; PAGE:en.w:Tintinan DATE:2015-08-02
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1234", "+1234"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1,234");
+	}
+	@Test public void Quantity__range() {	// PURPOSE:do not output ± if lbound == val == ubound; PAGE:en.w:Tintinan DATE:2015-08-02
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "1", "+1236", "+1233"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1,233-1,236");
+	}
+	@Test public void Quantity__long() {	// PURPOSE: must cast to long for large numbers; EX:{{#property:P1082}} PAGE:en.w:Earth; DATE:2015-08-02
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+4321000000", "1", "4321000000", "4321000000"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "4,321,000,000");
+	}
+	@Test public void Quantity__unit__entity() {// PURPOSE: get entity name; EX:{{#invoke:Wikidata|getUnits|P2386|FETCH_WIKIDATA}} PAGE:en.w:Arecibo_Observatory; DATE:2016-10-11
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+1234", "http://www.wikidata.org/entity/Q2", "+1236", "+1232"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Init__docs__add(fxt.Wdoc("Q2")				
+			.Add_claims(fxt.Make_claim_string(2, "a"))
+			.Add_label("en", "meter")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1,234±2 meter");
+	}
+	@Test public void Quantity__decimal() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_quantity(1, "+1234.50", "1", "+1236.75", "+1232.25"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "1,234.5±2.25");
+	}
+	@Test public void Monolingualtext() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_monolingual(1, "la", "Lorem ipsum dolor sit amet"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "Lorem ipsum dolor sit amet");
+	}
+	@Test public void Novalue() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_novalue(1))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "no value");
+	}
+	@Test public void Somevalue() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_somevalue(1))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "unknown value");
+	}
+	@Test public void Multiple() {
+		fxt.Init__docs__add(fxt.Wdoc("Q1")				
+			.Add_claims(fxt.Make_claim_string(1, "a"), fxt.Make_claim_string(1, "b"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1}}", "a");	// only take first; DATE:2015-08-02
+	}
+	@Test public void Q() {
+		fxt.Init__docs__add(fxt.Wdoc("Q2")				
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+
+		fxt.Test_parse("{{#property:p1|q=Q2}}", "a");
+	}
+	@Test public void Of() {
+		fxt.Init__docs__add(fxt.Wdoc("Q2")				
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			.Add_sitelink("enwiki", "Of_page")
+			);
+
+		fxt.Test_parse("{{#property:p1|of=Of_page}}", "a");
+	}
+	@Test public void From() {
+		fxt.Init__docs__add(fxt.Wdoc("Property:P2")
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			);
+
+		fxt.Test_parse("{{#property:p1|from=P2}}", "a");
+		fxt.Test_parse("{{#property:p1|from=}}", "");
+		fxt.Test_parse("{{#property:p1| from = P2 }}", "a"); // PURPOSE: trim ws; ISSUE#:361; DATE:2019-02-11
+	}
+	@Test public void Pid_as_name() {
+		fxt.Init__docs__add(fxt.Wdoc("Q2")
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+		fxt.Init_pids_add("en", "astronomic symbol", 1);
+
+		fxt.Test_parse("{{#property:astronomic symbol}}", "a");
+	}
+	@Test public void Empty_arg() {	// PURPOSE: {{#property:p1|}} should not fail / warn; DATE:2013-11-15
+		fxt.Init__docs__add(fxt.Wdoc("Q2")
+			.Add_claims(fxt.Make_claim_string(1, "a"))
+			.Add_sitelink("enwiki", "Test_page")
+			);
+		fxt.Init_pids_add("en", "astronomic symbol", 1);
+
+		fxt.Test_parse("{{#property:p1|}}", "a");
+	}
+	@Test public void Parse_pid() {
+		fxt.Test_parse_pid		("p123"	, 123);		// basic
+		fxt.Test_parse_pid		("P123"	, 123);		// uppercase
+		fxt.Test_parse_pid_null	("population");		// name test
+		fxt.Test_parse_pid_null	("123");			// missing p
+		fxt.Test_parse_pid_null	("");				// empty String test
+	}
+}