More complex algorithms in SQL are easier to write (and read) with CTE:
with
syntax as (
select str,
str ~ '^\d{4}-\d{2}-\d{2}$' as syntax_ok,
split_part(str, '-', 1) as syy,
split_part(str, '-', 2) as smm,
split_part(str, '-', 3) as sdd
from test_date),
toint as (
select *,
case when syntax_ok then syy::int else 1900 end yy,
case when syntax_ok then smm::int else 1 end mm,
case when syntax_ok then sdd::int else 1 end dd
from source),
semantics as (
select *,
case
when mm in (1,3,5,7,8,10,12) then dd between 1 and 31
when mm in (4,6,9,11) then dd between 1 and 30
when mm = 2 then
case when yy/4*4 = yy and (yy/100*100 <> yy or yy/400*400 = yy)
then dd between 1 and 29
else dd between 1 and 28 end
else false end as valid
from toint),
result as (
select str,
case when syntax_ok and valid then str
else '1900-01-01' end as date
from semantics)
select * from result
The first query checks syntax and split str into three parts, the second query casts the parts as integers, and the third query checks semantics.
SQLFiddle.
Your comment shows that you do not need quite as thoroughly checks, but I think that this query can be easily customized to your liking.