Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
fmt
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Libraries
fmt
Commits
687301c5
Commit
687301c5
authored
Jan 26, 2013
by
Victor Zverovich
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Parameterize BasicFormatter on char type.
parent
dbfd021a
Changes
3
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
334 additions
and
306 deletions
+334
-306
format.cc
format.cc
+13
-253
format.h
format.h
+284
-24
format_test.cc
format_test.cc
+37
-29
No files found.
format.cc
View file @
687301c5
...
...
@@ -64,32 +64,6 @@ using fmt::StringRef;
namespace
{
template
<
typename
T
>
struct
IsLongDouble
{
enum
{
VALUE
=
0
};
};
template
<
>
struct
IsLongDouble
<
long
double
>
{
enum
{
VALUE
=
1
};
};
const
char
DIGITS
[]
=
"0001020304050607080910111213141516171819"
"2021222324252627282930313233343536373839"
"4041424344454647484950515253545556575859"
"6061626364656667686970717273747576777879"
"8081828384858687888990919293949596979899"
;
// Fills the padding around the content and returns the pointer to the
// content area.
char
*
FillPadding
(
char
*
buffer
,
unsigned
total_size
,
std
::
size_t
content_size
,
char
fill
)
{
std
::
size_t
padding
=
total_size
-
content_size
;
std
::
size_t
left_padding
=
padding
/
2
;
std
::
fill_n
(
buffer
,
left_padding
,
fill
);
buffer
+=
left_padding
;
char
*
content
=
buffer
;
std
::
fill_n
(
buffer
+
content_size
,
padding
-
left_padding
,
fill
);
return
content
;
}
#ifdef _MSC_VER
int
SignBit
(
double
value
)
{
if
(
value
<
0
)
return
1
;
...
...
@@ -101,237 +75,23 @@ int SignBit(double value) {
#endif
}
void
BasicFormatter
::
FormatDecimal
(
char
*
buffer
,
uint64_t
value
,
unsigned
num_digits
)
{
--
num_digits
;
while
(
value
>=
100
)
{
// Integer division is slow so do it for a group of two digits instead
// of for every digit. The idea comes from the talk by Alexandrescu
// "Three Optimization Tips for C++". See speed-test for a comparison.
unsigned
index
=
(
value
%
100
)
*
2
;
value
/=
100
;
buffer
[
num_digits
]
=
DIGITS
[
index
+
1
];
buffer
[
num_digits
-
1
]
=
DIGITS
[
index
];
num_digits
-=
2
;
}
if
(
value
<
10
)
{
*
buffer
=
static_cast
<
char
>
(
'0'
+
value
);
return
;
}
unsigned
index
=
static_cast
<
unsigned
>
(
value
*
2
);
buffer
[
1
]
=
DIGITS
[
index
+
1
];
buffer
[
0
]
=
DIGITS
[
index
];
}
const
char
fmt
::
internal
::
DIGITS
[]
=
"0001020304050607080910111213141516171819"
"2021222324252627282930313233343536373839"
"4041424344454647484950515253545556575859"
"6061626364656667686970717273747576777879"
"8081828384858687888990919293949596979899"
;
void
BasicFormatter
::
ReportUnknownType
(
char
code
,
const
char
*
type
)
{
void
fmt
::
internal
::
ReportUnknownType
(
char
code
,
const
char
*
type
)
{
if
(
std
::
isprint
(
static_cast
<
unsigned
char
>
(
code
)))
{
throw
fmt
::
FormatError
(
fmt
::
str
(
fmt
::
Format
(
"unknown format code '{
0}' for {1
}"
)
<<
code
<<
type
));
fmt
::
Format
(
"unknown format code '{
}' for {
}"
)
<<
code
<<
type
));
}
throw
fmt
::
FormatError
(
fmt
::
str
(
fmt
::
Format
(
"unknown format code '
\\
x{
0:02x}' for {1
}"
)
fmt
::
str
(
fmt
::
Format
(
"unknown format code '
\\
x{
:02x}' for {
}"
)
<<
static_cast
<
unsigned
>
(
code
)
<<
type
));
}
char
*
BasicFormatter
::
PrepareFilledBuffer
(
unsigned
size
,
const
AlignSpec
&
spec
,
char
sign
)
{
unsigned
width
=
spec
.
width
();
if
(
width
<=
size
)
{
char
*
p
=
GrowBuffer
(
size
);
*
p
=
sign
;
return
p
+
size
-
1
;
}
char
*
p
=
GrowBuffer
(
width
);
char
*
end
=
p
+
width
;
Alignment
align
=
spec
.
align
();
if
(
align
==
ALIGN_LEFT
)
{
*
p
=
sign
;
p
+=
size
;
std
::
fill
(
p
,
end
,
spec
.
fill
());
}
else
if
(
align
==
ALIGN_CENTER
)
{
p
=
FillPadding
(
p
,
width
,
size
,
spec
.
fill
());
*
p
=
sign
;
p
+=
size
;
}
else
{
if
(
align
==
ALIGN_NUMERIC
)
{
if
(
sign
)
{
*
p
++
=
sign
;
--
size
;
}
}
else
{
*
(
end
-
size
)
=
sign
;
}
std
::
fill
(
p
,
end
-
size
,
spec
.
fill
());
p
=
end
;
}
return
p
-
1
;
}
template
<
typename
T
>
void
BasicFormatter
::
FormatDouble
(
T
value
,
const
FormatSpec
&
spec
,
int
precision
)
{
// Check type.
char
type
=
spec
.
type
();
bool
upper
=
false
;
switch
(
type
)
{
case
0
:
type
=
'g'
;
break
;
case
'e'
:
case
'f'
:
case
'g'
:
break
;
case
'F'
:
#ifdef _MSC_VER
// MSVC's printf doesn't support 'F'.
type
=
'f'
;
#endif
// Fall through.
case
'E'
:
case
'G'
:
upper
=
true
;
break
;
default:
ReportUnknownType
(
type
,
"double"
);
break
;
}
char
sign
=
0
;
// Use SignBit instead of value < 0 because the latter is always
// false for NaN.
if
(
SignBit
(
value
))
{
sign
=
'-'
;
value
=
-
value
;
}
else
if
(
spec
.
sign_flag
())
{
sign
=
spec
.
plus_flag
()
?
'+'
:
' '
;
}
if
(
value
!=
value
)
{
// Format NaN ourselves because sprintf's output is not consistent
// across platforms.
std
::
size_t
size
=
4
;
const
char
*
nan
=
upper
?
" NAN"
:
" nan"
;
if
(
!
sign
)
{
--
size
;
++
nan
;
}
char
*
out
=
FormatString
(
nan
,
size
,
spec
);
if
(
sign
)
*
out
=
sign
;
return
;
}
if
(
isinf
(
value
))
{
// Format infinity ourselves because sprintf's output is not consistent
// across platforms.
std
::
size_t
size
=
4
;
const
char
*
inf
=
upper
?
" INF"
:
" inf"
;
if
(
!
sign
)
{
--
size
;
++
inf
;
}
char
*
out
=
FormatString
(
inf
,
size
,
spec
);
if
(
sign
)
*
out
=
sign
;
return
;
}
size_t
offset
=
buffer_
.
size
();
unsigned
width
=
spec
.
width
();
if
(
sign
)
{
buffer_
.
reserve
(
buffer_
.
size
()
+
std
::
max
(
width
,
1u
));
if
(
width
>
0
)
--
width
;
++
offset
;
}
// Build format string.
enum
{
MAX_FORMAT_SIZE
=
10
};
// longest format: %#-*.*Lg
char
format
[
MAX_FORMAT_SIZE
];
char
*
format_ptr
=
format
;
*
format_ptr
++
=
'%'
;
unsigned
width_for_sprintf
=
width
;
if
(
spec
.
hash_flag
())
*
format_ptr
++
=
'#'
;
if
(
spec
.
align
()
==
ALIGN_CENTER
)
{
width_for_sprintf
=
0
;
}
else
{
if
(
spec
.
align
()
==
ALIGN_LEFT
)
*
format_ptr
++
=
'-'
;
if
(
width
!=
0
)
*
format_ptr
++
=
'*'
;
}
if
(
precision
>=
0
)
{
*
format_ptr
++
=
'.'
;
*
format_ptr
++
=
'*'
;
}
if
(
IsLongDouble
<
T
>::
VALUE
)
*
format_ptr
++
=
'L'
;
*
format_ptr
++
=
type
;
*
format_ptr
=
'\0'
;
// Format using snprintf.
for
(;;)
{
size_t
size
=
buffer_
.
capacity
()
-
offset
;
int
n
=
0
;
char
*
start
=
&
buffer_
[
offset
];
if
(
width_for_sprintf
==
0
)
{
n
=
precision
<
0
?
snprintf
(
start
,
size
,
format
,
value
)
:
snprintf
(
start
,
size
,
format
,
precision
,
value
);
}
else
{
n
=
precision
<
0
?
snprintf
(
start
,
size
,
format
,
width_for_sprintf
,
value
)
:
snprintf
(
start
,
size
,
format
,
width_for_sprintf
,
precision
,
value
);
}
if
(
n
>=
0
&&
offset
+
n
<
buffer_
.
capacity
())
{
if
(
sign
)
{
if
((
spec
.
align
()
!=
ALIGN_RIGHT
&&
spec
.
align
()
!=
ALIGN_DEFAULT
)
||
*
start
!=
' '
)
{
*
(
start
-
1
)
=
sign
;
sign
=
0
;
}
else
{
*
(
start
-
1
)
=
spec
.
fill
();
}
++
n
;
}
if
(
spec
.
align
()
==
ALIGN_CENTER
&&
spec
.
width
()
>
static_cast
<
unsigned
>
(
n
))
{
char
*
p
=
GrowBuffer
(
spec
.
width
());
std
::
copy
(
p
,
p
+
n
,
p
+
(
spec
.
width
()
-
n
)
/
2
);
FillPadding
(
p
,
spec
.
width
(),
n
,
spec
.
fill
());
return
;
}
if
(
spec
.
fill
()
!=
' '
||
sign
)
{
while
(
*
start
==
' '
)
*
start
++
=
spec
.
fill
();
if
(
sign
)
*
(
start
-
1
)
=
sign
;
}
GrowBuffer
(
n
);
return
;
}
buffer_
.
reserve
(
n
>=
0
?
offset
+
n
+
1
:
2
*
buffer_
.
capacity
());
}
}
char
*
BasicFormatter
::
FormatString
(
const
char
*
s
,
std
::
size_t
size
,
const
FormatSpec
&
spec
)
{
char
*
out
=
0
;
if
(
spec
.
width
()
>
size
)
{
out
=
GrowBuffer
(
spec
.
width
());
if
(
spec
.
align
()
==
ALIGN_RIGHT
)
{
std
::
fill_n
(
out
,
spec
.
width
()
-
size
,
spec
.
fill
());
out
+=
spec
.
width
()
-
size
;
}
else
if
(
spec
.
align
()
==
ALIGN_CENTER
)
{
out
=
FillPadding
(
out
,
spec
.
width
(),
size
,
spec
.
fill
());
}
else
{
std
::
fill_n
(
out
+
size
,
spec
.
width
()
-
size
,
spec
.
fill
());
}
}
else
{
out
=
GrowBuffer
(
size
);
}
std
::
copy
(
s
,
s
+
size
,
out
);
return
out
;
}
// Throws Exception(message) if format contains '}', otherwise throws
// FormatError reporting unmatched '{'. The idea is that unmatched '{'
// should override other errors.
...
...
@@ -573,7 +333,7 @@ void Formatter::DoFormat() {
break
;
case
CHAR
:
{
if
(
spec
.
type_
&&
spec
.
type_
!=
'c'
)
ReportUnknownType
(
spec
.
type_
,
"char"
);
internal
::
ReportUnknownType
(
spec
.
type_
,
"char"
);
char
*
out
=
0
;
if
(
spec
.
width_
>
1
)
{
out
=
GrowBuffer
(
spec
.
width_
);
...
...
@@ -593,7 +353,7 @@ void Formatter::DoFormat() {
}
case
STRING
:
{
if
(
spec
.
type_
&&
spec
.
type_
!=
's'
)
ReportUnknownType
(
spec
.
type_
,
"string"
);
internal
::
ReportUnknownType
(
spec
.
type_
,
"string"
);
const
char
*
str
=
arg
.
string
.
value
;
size_t
size
=
arg
.
string
.
size
;
if
(
size
==
0
)
{
...
...
@@ -607,14 +367,14 @@ void Formatter::DoFormat() {
}
case
POINTER
:
if
(
spec
.
type_
&&
spec
.
type_
!=
'p'
)
ReportUnknownType
(
spec
.
type_
,
"pointer"
);
internal
::
ReportUnknownType
(
spec
.
type_
,
"pointer"
);
spec
.
flags_
=
HASH_FLAG
;
spec
.
type_
=
'x'
;
FormatInt
(
reinterpret_cast
<
uintptr_t
>
(
arg
.
pointer_value
),
spec
);
break
;
case
CUSTOM
:
if
(
spec
.
type_
)
ReportUnknownType
(
spec
.
type_
,
"object"
);
internal
::
ReportUnknownType
(
spec
.
type_
,
"object"
);
(
this
->*
arg
.
custom
.
format
)(
arg
.
custom
.
value
,
spec
);
break
;
default:
...
...
format.h
View file @
687301c5
This diff is collapsed.
Click to expand it.
format_test.cc
View file @
687301c5
...
...
@@ -862,7 +862,9 @@ class Date {
return
os
;
}
friend
BasicFormatter
&
operator
<<
(
BasicFormatter
&
f
,
const
Date
&
d
)
{
template
<
typename
Char
>
friend
BasicFormatter
<
Char
>
&
operator
<<
(
BasicFormatter
<
Char
>
&
f
,
const
Date
&
d
)
{
return
f
<<
d
.
year_
<<
'-'
<<
d
.
month_
<<
'-'
<<
d
.
day_
;
}
};
...
...
@@ -877,7 +879,8 @@ TEST(FormatterTest, FormatUsingIOStreams) {
class
Answer
{};
void
Format
(
fmt
::
BasicFormatter
&
f
,
const
fmt
::
FormatSpec
&
spec
,
Answer
)
{
template
<
typename
Char
>
void
Format
(
fmt
::
BasicFormatter
<
Char
>
&
f
,
const
fmt
::
FormatSpec
&
spec
,
Answer
)
{
f
.
Write
(
"42"
,
spec
);
}
...
...
@@ -918,7 +921,7 @@ TEST(FormatterTest, FormatterAppend) {
TEST
(
FormatterTest
,
FormatterExamples
)
{
using
fmt
::
hex
;
EXPECT_EQ
(
"0000cafe"
,
str
(
BasicFormatter
()
<<
pad
(
hex
(
0xcafe
),
8
,
'0'
)));
EXPECT_EQ
(
"0000cafe"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
hex
(
0xcafe
),
8
,
'0'
)));
std
::
string
message
=
str
(
Format
(
"The answer is {}"
)
<<
42
);
EXPECT_EQ
(
"The answer is 42"
,
message
);
...
...
@@ -1068,11 +1071,11 @@ TEST(TempFormatterTest, Examples) {
TEST
(
StrTest
,
oct
)
{
using
fmt
::
oct
;
EXPECT_EQ
(
"12"
,
(
BasicFormatter
()
<<
oct
(
static_cast
<
short
>
(
012
))).
str
(
));
EXPECT_EQ
(
"12"
,
(
BasicFormatter
()
<<
oct
(
012
)).
str
(
));
EXPECT_EQ
(
"34"
,
(
BasicFormatter
()
<<
oct
(
034u
)).
str
(
));
EXPECT_EQ
(
"56"
,
(
BasicFormatter
()
<<
oct
(
056l
)).
str
(
));
EXPECT_EQ
(
"70"
,
(
BasicFormatter
()
<<
oct
(
070ul
)).
str
(
));
EXPECT_EQ
(
"12"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
static_cast
<
short
>
(
012
))
));
EXPECT_EQ
(
"12"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
012
)
));
EXPECT_EQ
(
"34"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
034u
)
));
EXPECT_EQ
(
"56"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
056l
)
));
EXPECT_EQ
(
"70"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
070ul
)
));
}
TEST
(
StrTest
,
hex
)
{
...
...
@@ -1082,17 +1085,17 @@ TEST(StrTest, hex) {
// This shouldn't compile:
//fmt::IntFormatter<short, fmt::TypeSpec<'x'> > (*phex2)(short value) = hex;
EXPECT_EQ
(
"cafe"
,
(
BasicFormatter
()
<<
hex
(
0xcafe
)).
str
(
));
EXPECT_EQ
(
"babe"
,
(
BasicFormatter
()
<<
hex
(
0xbabeu
)).
str
(
));
EXPECT_EQ
(
"dead"
,
(
BasicFormatter
()
<<
hex
(
0xdeadl
)).
str
(
));
EXPECT_EQ
(
"beef"
,
(
BasicFormatter
()
<<
hex
(
0xbeeful
)).
str
(
));
EXPECT_EQ
(
"cafe"
,
str
(
BasicFormatter
<
char
>
()
<<
hex
(
0xcafe
)
));
EXPECT_EQ
(
"babe"
,
str
(
BasicFormatter
<
char
>
()
<<
hex
(
0xbabeu
)
));
EXPECT_EQ
(
"dead"
,
str
(
BasicFormatter
<
char
>
()
<<
hex
(
0xdeadl
)
));
EXPECT_EQ
(
"beef"
,
str
(
BasicFormatter
<
char
>
()
<<
hex
(
0xbeeful
)
));
}
TEST
(
StrTest
,
hexu
)
{
EXPECT_EQ
(
"CAFE"
,
(
BasicFormatter
()
<<
hexu
(
0xcafe
)).
str
(
));
EXPECT_EQ
(
"BABE"
,
(
BasicFormatter
()
<<
hexu
(
0xbabeu
)).
str
(
));
EXPECT_EQ
(
"DEAD"
,
(
BasicFormatter
()
<<
hexu
(
0xdeadl
)).
str
(
));
EXPECT_EQ
(
"BEEF"
,
(
BasicFormatter
()
<<
hexu
(
0xbeeful
)).
str
(
));
EXPECT_EQ
(
"CAFE"
,
str
(
BasicFormatter
<
char
>
()
<<
hexu
(
0xcafe
)
));
EXPECT_EQ
(
"BABE"
,
str
(
BasicFormatter
<
char
>
()
<<
hexu
(
0xbabeu
)
));
EXPECT_EQ
(
"DEAD"
,
str
(
BasicFormatter
<
char
>
()
<<
hexu
(
0xdeadl
)
));
EXPECT_EQ
(
"BEEF"
,
str
(
BasicFormatter
<
char
>
()
<<
hexu
(
0xbeeful
)
));
}
class
ISO8601DateFormatter
{
...
...
@@ -1101,8 +1104,9 @@ class ISO8601DateFormatter {
public:
ISO8601DateFormatter
(
const
Date
&
d
)
:
date_
(
&
d
)
{}
friend
BasicFormatter
&
operator
<<
(
BasicFormatter
&
f
,
const
ISO8601DateFormatter
&
d
)
{
template
<
typename
Char
>
friend
BasicFormatter
<
Char
>
&
operator
<<
(
BasicFormatter
<
Char
>
&
f
,
const
ISO8601DateFormatter
&
d
)
{
return
f
<<
pad
(
d
.
date_
->
year
(),
4
,
'0'
)
<<
'-'
<<
pad
(
d
.
date_
->
month
(),
2
,
'0'
)
<<
'-'
<<
pad
(
d
.
date_
->
day
(),
2
,
'0'
);
}
...
...
@@ -1112,17 +1116,17 @@ ISO8601DateFormatter iso8601(const Date &d) { return ISO8601DateFormatter(d); }
TEST
(
StrTest
,
pad
)
{
using
fmt
::
hex
;
EXPECT_EQ
(
" cafe"
,
(
BasicFormatter
()
<<
pad
(
hex
(
0xcafe
),
8
)).
str
(
));
EXPECT_EQ
(
" babe"
,
(
BasicFormatter
()
<<
pad
(
hex
(
0xbabeu
),
8
)).
str
(
));
EXPECT_EQ
(
" dead"
,
(
BasicFormatter
()
<<
pad
(
hex
(
0xdeadl
),
8
)).
str
(
));
EXPECT_EQ
(
" beef"
,
(
BasicFormatter
()
<<
pad
(
hex
(
0xbeeful
),
8
)).
str
(
));
EXPECT_EQ
(
" cafe"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
hex
(
0xcafe
),
8
)
));
EXPECT_EQ
(
" babe"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
hex
(
0xbabeu
),
8
)
));
EXPECT_EQ
(
" dead"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
hex
(
0xdeadl
),
8
)
));
EXPECT_EQ
(
" beef"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
hex
(
0xbeeful
),
8
)
));
EXPECT_EQ
(
" 11"
,
(
BasicFormatter
()
<<
pad
(
11
,
7
)).
str
(
));
EXPECT_EQ
(
" 22"
,
(
BasicFormatter
()
<<
pad
(
22u
,
7
)).
str
(
));
EXPECT_EQ
(
" 33"
,
(
BasicFormatter
()
<<
pad
(
33l
,
7
)).
str
(
));
EXPECT_EQ
(
" 44"
,
(
BasicFormatter
()
<<
pad
(
44lu
,
7
)).
str
(
));
EXPECT_EQ
(
" 11"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
11
,
7
)
));
EXPECT_EQ
(
" 22"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
22u
,
7
)
));
EXPECT_EQ
(
" 33"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
33l
,
7
)
));
EXPECT_EQ
(
" 44"
,
str
(
BasicFormatter
<
char
>
()
<<
pad
(
44lu
,
7
)
));
BasicFormatter
f
;
BasicFormatter
<
char
>
f
;
f
.
Clear
();
f
<<
pad
(
42
,
5
,
'0'
);
EXPECT_EQ
(
"00042"
,
f
.
str
());
...
...
@@ -1137,8 +1141,12 @@ TEST(StrTest, pad) {
TEST
(
StrTest
,
NoConflictWithIOManip
)
{
using
namespace
std
;
using
namespace
fmt
;
EXPECT_EQ
(
"cafe"
,
(
BasicFormatter
()
<<
hex
(
0xcafe
)).
str
());
EXPECT_EQ
(
"12"
,
(
BasicFormatter
()
<<
oct
(
012
)).
str
());
EXPECT_EQ
(
"cafe"
,
str
(
BasicFormatter
<
char
>
()
<<
hex
(
0xcafe
)));
EXPECT_EQ
(
"12"
,
str
(
BasicFormatter
<
char
>
()
<<
oct
(
012
)));
}
TEST
(
StrTest
,
BasicFormatterWChar
)
{
EXPECT_EQ
(
L"cafe"
,
str
(
BasicFormatter
<
wchar_t
>
()
<<
fmt
::
hex
(
0xcafe
)));
}
template
<
typename
T
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment